From 6b6f77a2a15ef646e3ebf68d46785267c93ca89c Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Tue, 9 Dec 2025 23:02:46 +0300 Subject: [PATCH 01/23] fix: changed name from violations to error because of tests --- .../shareit/exception/dto/ValidationErrorResponse.java | 2 +- .../ru/practicum/shareit/request/dto/ItemRequestDto.java | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java diff --git a/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java b/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java index 497330f..a9b60d8 100644 --- a/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java +++ b/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java @@ -2,5 +2,5 @@ import java.util.List; -public record ValidationErrorResponse(List violations) { +public record ValidationErrorResponse(List error) { //violations are better name but test expect error } diff --git a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java b/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java deleted file mode 100644 index 7b3ed54..0000000 --- a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.request.dto; - -/** - * TODO Sprint add-item-requests. - */ -public class ItemRequestDto { -} From 434feb022882664f11e2602f83bbed50889b0b19 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Tue, 9 Dec 2025 23:03:13 +0300 Subject: [PATCH 02/23] feat: add item requests --- .../shareit/item/ItemServiceImpl.java | 9 ++ .../shareit/item/dto/ItemForRequestDto.java | 4 + .../shareit/item/dto/NewItemDto.java | 4 +- .../shareit/item/mapper/ItemMapper.java | 11 ++- .../ru/practicum/shareit/item/model/Item.java | 5 +- .../item/repository/ItemRepository.java | 5 ++ .../request/ItemRequestController.java | 44 ++++++++-- .../shareit/request/ItemRequestMapper.java | 56 +++++++++++++ .../shareit/request/dto/ItemRequestDto.java | 6 ++ .../dto/ItemRequestWithResponsesDto.java | 14 ++++ .../request/dto/NewItemRequestDto.java | 8 ++ .../shareit/request/model/ItemRequest.java | 29 +++++-- .../repository/ItemRequestRepository.java | 12 +++ .../request/service/ItemRequestService.java | 17 ++++ .../service/ItemRequestServiceImpl.java | 84 +++++++++++++++++++ src/main/resources/application.properties | 2 +- src/main/resources/schema.sql | 14 +++- 17 files changed, 304 insertions(+), 20 deletions(-) create mode 100644 src/main/java/ru/practicum/shareit/item/dto/ItemForRequestDto.java create mode 100644 src/main/java/ru/practicum/shareit/request/ItemRequestMapper.java create mode 100644 src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java create mode 100644 src/main/java/ru/practicum/shareit/request/dto/ItemRequestWithResponsesDto.java create mode 100644 src/main/java/ru/practicum/shareit/request/dto/NewItemRequestDto.java create mode 100644 src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java create mode 100644 src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java create mode 100644 src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java index 7ed4a50..2c7fcb2 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -17,6 +17,8 @@ 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; @@ -33,6 +35,7 @@ public class ItemServiceImpl implements ItemService { private final UserRepository userRepository; private final BookingRepository bookingRepository; private final CommentRepository commentRepository; + private final ItemRequestRepository requestRepository; @Override public ItemWithBookingDto getItemOfUserById(long userId, long itemId) { @@ -81,6 +84,12 @@ public ItemDto saveItem(long userId, NewItemDto newItem) { User owner = getUserOrThrow(userId); Item item = ItemMapper.toItem(newItem); item.setOwner(owner); + if (newItem.requestId() != null) { + ItemRequest request = requestRepository.findById(newItem.requestId()).orElseThrow( + NotFoundException.supplier("Request with id %d not found", newItem.requestId()) + ); + item.setRequest(request); + } item = itemRepository.save(item); return ItemMapper.toItemDto(item); } diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemForRequestDto.java b/src/main/java/ru/practicum/shareit/item/dto/ItemForRequestDto.java new file mode 100644 index 0000000..b0610d9 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemForRequestDto.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.item.dto; + +public record ItemForRequestDto(Long id, String name, Long ownerId) { +} diff --git a/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java b/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java index 65648c3..a7d9e6c 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java @@ -1,11 +1,13 @@ package ru.practicum.shareit.item.dto; +import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; public record NewItemDto( @NotBlank String name, @NotBlank String description, - @NotNull Boolean available + @NotNull Boolean available, + @Nullable Long requestId ) { } diff --git a/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java index 4020849..0371351 100644 --- a/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java @@ -4,10 +4,7 @@ import ru.practicum.shareit.booking.BookingMapper; import ru.practicum.shareit.booking.dto.BookingInfoDto; import ru.practicum.shareit.booking.model.Booking; -import ru.practicum.shareit.item.dto.ItemDto; -import ru.practicum.shareit.item.dto.ItemWithBookingDto; -import ru.practicum.shareit.item.dto.NewItemDto; -import ru.practicum.shareit.item.dto.UpdateItemDto; +import ru.practicum.shareit.item.dto.*; import ru.practicum.shareit.item.model.Comment; import ru.practicum.shareit.item.model.Item; @@ -70,4 +67,10 @@ public static ItemWithBookingDto toItemWithBookingDatesDto( ); } + public static ItemForRequestDto toItemForRequestDto(Item item) { + Long itemId = item.getId(); + String itemName = item.getName(); + return new ItemForRequestDto(itemId, itemName, item.getOwner().getId()); + } + } diff --git a/src/main/java/ru/practicum/shareit/item/model/Item.java b/src/main/java/ru/practicum/shareit/item/model/Item.java index 379bd9f..007a88f 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import lombok.*; import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.request.model.ItemRequest; import ru.practicum.shareit.user.model.User; @Entity @@ -28,5 +29,7 @@ public class Item { @JoinColumn(name = "owner_id") User owner; - // ItemRequest request; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "request_id") + ItemRequest request; } diff --git a/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java b/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java index c911140..d12b9ac 100644 --- a/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java +++ b/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.jpa.repository.Query; import ru.practicum.shareit.item.model.Item; +import java.util.Collection; import java.util.List; public interface ItemRepository extends JpaRepository { @@ -16,4 +17,8 @@ public interface ItemRepository extends JpaRepository { or lower(i.description) like lower(concat('%', :query, '%')) """) List search(String query); + + List findAllByRequest_Id(Long requestId); + + List findAllByRequest_IdIn(Collection requestIds); } diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequestController.java b/src/main/java/ru/practicum/shareit/request/ItemRequestController.java index 064e2e9..efb5830 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.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestWithResponsesDto; +import ru.practicum.shareit.request.dto.NewItemRequestDto; +import ru.practicum.shareit.request.service.ItemRequestService; -/** - * TODO Sprint add-item-requests. - */ +import java.util.Collection; + +@Validated @RestController -@RequestMapping(path = "/requests") +@RequiredArgsConstructor +@RequestMapping("/requests") public class ItemRequestController { + private static final String SHARER_USER_ID_HEADER = "X-Sharer-User-Id"; + private final ItemRequestService itemRequestService; + + @PostMapping + public ItemRequestDto createRequest( + @RequestHeader(SHARER_USER_ID_HEADER) long requestorId, + @Valid @RequestBody NewItemRequestDto newItemRequestDto + ) { + return itemRequestService.create(requestorId, newItemRequestDto); + } + + @GetMapping + public Collection getAllRequestsOfUser(@RequestHeader(SHARER_USER_ID_HEADER) long userId) { + return itemRequestService.getAllOfUser(userId); + } + + @GetMapping("/all") + public Collection getAllRequests() { + return itemRequestService.getAll(); + } + + @GetMapping("/{requestId}") + public ItemRequestWithResponsesDto getRequestById(@PathVariable long requestId) { + return itemRequestService.getById(requestId); + } } diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequestMapper.java b/src/main/java/ru/practicum/shareit/request/ItemRequestMapper.java new file mode 100644 index 0000000..62054f3 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/request/ItemRequestMapper.java @@ -0,0 +1,56 @@ +package ru.practicum.shareit.request; + +import lombok.experimental.UtilityClass; +import ru.practicum.shareit.item.dto.ItemForRequestDto; +import ru.practicum.shareit.item.mapper.ItemMapper; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestWithResponsesDto; +import ru.practicum.shareit.request.dto.NewItemRequestDto; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.List; + +@UtilityClass +public class ItemRequestMapper { + public ItemRequestWithResponsesDto toItemRequestWithResponseDto(ItemRequest itemRequest, List items) { + Long itemRequestId = itemRequest.getId(); + String itemRequestDescription = itemRequest.getDescription(); + LocalDateTime itemRequestCreatedAt = itemRequest.getCreatedAt(); + List itemDtos = items == null ? null : items.stream() + .map(ItemMapper::toItemForRequestDto) + .toList(); + return new ItemRequestWithResponsesDto( + itemRequestId, + itemRequestDescription, + itemRequestCreatedAt, + itemDtos + ); + } + + + public ItemRequest toEntity(NewItemRequestDto newItemRequestDto, User requestor, LocalDateTime createdAt) { + return new ItemRequest( + null, + newItemRequestDto.description(), + requestor, + createdAt + ); + } + + public static ItemRequestDto toItemRequestDto(ItemRequest itemRequest) { + Long itemRequestId = itemRequest.getId(); + String itemRequestDescription = itemRequest.getDescription(); + Long requestorId = itemRequest.getRequestor().getId(); + LocalDateTime itemRequestCreatedAt = itemRequest.getCreatedAt(); + return new ItemRequestDto( + itemRequestId, + itemRequestDescription, + requestorId, + itemRequestCreatedAt + ); + } + +} diff --git a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java b/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java new file mode 100644 index 0000000..1ab4e88 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java @@ -0,0 +1,6 @@ +package ru.practicum.shareit.request.dto; + +import java.time.LocalDateTime; + +public record ItemRequestDto(Long id, String description, long requestorId, LocalDateTime created) { +} diff --git a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestWithResponsesDto.java b/src/main/java/ru/practicum/shareit/request/dto/ItemRequestWithResponsesDto.java new file mode 100644 index 0000000..b186655 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/request/dto/ItemRequestWithResponsesDto.java @@ -0,0 +1,14 @@ +package ru.practicum.shareit.request.dto; + +import ru.practicum.shareit.item.dto.ItemForRequestDto; + +import java.time.LocalDateTime; +import java.util.List; + +public record ItemRequestWithResponsesDto( + Long id, + String description, + LocalDateTime created, + List items +) { +} diff --git a/src/main/java/ru/practicum/shareit/request/dto/NewItemRequestDto.java b/src/main/java/ru/practicum/shareit/request/dto/NewItemRequestDto.java new file mode 100644 index 0000000..5ec7e6f --- /dev/null +++ b/src/main/java/ru/practicum/shareit/request/dto/NewItemRequestDto.java @@ -0,0 +1,8 @@ +package ru.practicum.shareit.request.dto; + +import jakarta.validation.constraints.NotBlank; + +public record NewItemRequestDto( + @NotBlank String description +) { +} 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 6f4ab59..d6f9671 100644 --- a/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java +++ b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java @@ -1,14 +1,31 @@ package ru.practicum.shareit.request.model; -import lombok.AccessLevel; -import lombok.Data; +import jakarta.persistence.*; +import lombok.*; import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.user.model.User; -/** - * TODO Sprint add-item-requests. - */ -@Data +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "requests") @FieldDefaults(level = AccessLevel.PRIVATE) public class ItemRequest { + @Id + @Column(name = "request_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; + + String description; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "requestor_id") + User requestor; + + @Column(name = "created_at") + LocalDateTime createdAt; } diff --git a/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java b/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java new file mode 100644 index 0000000..eb895a0 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java @@ -0,0 +1,12 @@ +package ru.practicum.shareit.request.repository; + +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 id); + + List findAllByRequestor_IdOrderByCreatedAtDesc(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 new file mode 100644 index 0000000..2c81b18 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java @@ -0,0 +1,17 @@ +package ru.practicum.shareit.request.service; + +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestWithResponsesDto; +import ru.practicum.shareit.request.dto.NewItemRequestDto; + +import java.util.Collection; + +public interface ItemRequestService { + ItemRequestDto create(long requestorId, NewItemRequestDto newItemRequestDto); + + Collection getAllOfUser(long userId); + + Collection getAll(); + + ItemRequestWithResponsesDto getById(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 new file mode 100644 index 0000000..81c4bd1 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java @@ -0,0 +1,84 @@ +package ru.practicum.shareit.request.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.request.ItemRequestMapper; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestWithResponsesDto; +import ru.practicum.shareit.request.dto.NewItemRequestDto; +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.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ItemRequestServiceImpl implements ItemRequestService { + private final ItemRequestRepository requestRepository; + private final UserRepository userRepository; + private final ItemRepository itemRepository; + + @Override + @Transactional + public ItemRequestDto create(long requestorId, NewItemRequestDto newItemRequestDto) { + User requestor = getUserOrThrow(requestorId); + LocalDateTime now = LocalDateTime.now(); + + ItemRequest itemRequest = ItemRequestMapper.toEntity(newItemRequestDto, requestor, now); + ItemRequest savedRequest = requestRepository.save(itemRequest); + return ItemRequestMapper.toItemRequestDto(savedRequest); + } + + @Override + public Collection getAllOfUser(long userId) { + getUserOrThrow(userId); + List requestsOfUser = + requestRepository.findAllByRequestor_IdOrderByCreatedAtDesc(userId); + List requestsIds = requestsOfUser.stream() + .map(ItemRequest::getId) + .toList(); + List itemResponsesForRequests = itemRepository.findAllByRequest_IdIn(requestsIds); + Map> itemsByRequestId = itemResponsesForRequests.stream() + .collect(Collectors.groupingBy(Item::getId)); + return requestsOfUser.stream() + .map(itemRequest -> ItemRequestMapper.toItemRequestWithResponseDto( + itemRequest, + itemsByRequestId.get(itemRequest.getId()) + )) + .toList(); + } + + @Override + public Collection getAll() { + List requests = requestRepository.findAll(); + return requests.stream() + .map(ItemRequestMapper::toItemRequestDto) + .toList(); + } + + @Override + public ItemRequestWithResponsesDto getById(long requestId) { + ItemRequest request = requestRepository.findById(requestId).orElseThrow( + NotFoundException.supplier("Request with id %d not found", requestId) + ); + List itemResponses = itemRepository.findAllByRequest_Id(requestId); + return ItemRequestMapper.toItemRequestWithResponseDto(request, itemResponses); + } + + private User getUserOrThrow(long userId) { + return userRepository.findById(userId).orElseThrow( + NotFoundException.supplier("User with id %d not found", userId) + ); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a627601..473341c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,4 @@ -spring.jpa.hibernate.ddl-auto=validate +spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.format_sql=true spring.sql.init.mode=always diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 67db65a..72855fd 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -7,6 +7,16 @@ CREATE TABLE IF NOT EXISTS users CONSTRAINT unique_user_email UNIQUE (email) ); +CREATE TABLE IF NOT EXISTS requests +( + request_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + description VARCHAR(1024) NOT NULL, + requestor_id BIGINT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + CONSTRAINT fk_requestor FOREIGN KEY (requestor_id) REFERENCES users(user_id) ON DELETE CASCADE +); + CREATE TABLE IF NOT EXISTS items ( item_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, @@ -14,8 +24,10 @@ CREATE TABLE IF NOT EXISTS items description VARCHAR(1024) NOT NULL, available BOOLEAN NOT NULL, owner_id BIGINT NOT NULL, + request_id BIGINT, - CONSTRAINT fk_item_owner FOREIGN KEY (owner_id) REFERENCES users(user_id) ON DELETE CASCADE + CONSTRAINT fk_item_owner FOREIGN KEY (owner_id) REFERENCES users(user_id) ON DELETE CASCADE, + CONSTRAINT fk_item_request FOREIGN KEY (request_id) REFERENCES requests(request_id) ); CREATE TABLE IF NOT EXISTS bookings From cba4ce3cb7435e0dfc527520571ee6745b0e1993 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Tue, 9 Dec 2025 23:08:54 +0300 Subject: [PATCH 03/23] chore: added sorting to itemRequest find all call --- .../shareit/request/service/ItemRequestServiceImpl.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 81c4bd1..5c56c0f 100644 --- a/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java @@ -1,6 +1,7 @@ package ru.practicum.shareit.request.service; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.practicum.shareit.exception.NotFoundException; @@ -48,9 +49,11 @@ public Collection getAllOfUser(long userId) { List requestsIds = requestsOfUser.stream() .map(ItemRequest::getId) .toList(); + List itemResponsesForRequests = itemRepository.findAllByRequest_IdIn(requestsIds); Map> itemsByRequestId = itemResponsesForRequests.stream() .collect(Collectors.groupingBy(Item::getId)); + return requestsOfUser.stream() .map(itemRequest -> ItemRequestMapper.toItemRequestWithResponseDto( itemRequest, @@ -61,7 +64,8 @@ public Collection getAllOfUser(long userId) { @Override public Collection getAll() { - List requests = requestRepository.findAll(); + List requests = + requestRepository.findAll(Sort.by(Sort.Direction.DESC, "createdAt")); return requests.stream() .map(ItemRequestMapper::toItemRequestDto) .toList(); From b40e2fcaab02faccecf6e2b239485ed27c3d748a Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Wed, 10 Dec 2025 01:32:22 +0300 Subject: [PATCH 04/23] feat: add modules separation --- docker-compose.yaml | 34 +++++++++-- gateway/Dockerfile | 40 +++++++++++++ gateway/pom.xml | 25 ++++++++ .../ru/practicum/shareit/ShareItGateway.java | 12 ++++ .../src/main/resources/application.properties | 1 + .../practicum/shareit/ShareItGatewayTest.java | 13 +++++ pom.xml | 24 ++------ Dockerfile => server/Dockerfile | 11 ++-- server/pom.xml | 58 +++++++++++++++++++ .../ru/practicum/shareit/ShareItServer.java | 4 +- .../shareit/booking/BookingController.java | 0 .../shareit/booking/BookingMapper.java | 0 .../shareit/booking/BookingService.java | 0 .../shareit/booking/BookingServiceImpl.java | 0 .../shareit/booking/BookingState.java | 0 .../shareit/booking/dto/BookingDto.java | 0 .../shareit/booking/dto/BookingInfoDto.java | 0 .../booking/dto/BookingResponseDto.java | 0 .../shareit/booking/model/Booking.java | 0 .../shareit/booking/model/BookingStatus.java | 0 .../booking/repository/BookingRepository.java | 0 .../BookingIntersectionException.java | 0 .../exception/DuplicateDataException.java | 0 .../exception/ForbiddenAccessException.java | 0 .../exception/ItemCommentException.java | 0 .../exception/ItemUnavailableException.java | 0 .../shareit/exception/NotFoundException.java | 0 .../shareit/exception/dto/ErrorResponse.java | 0 .../dto/ValidationErrorResponse.java | 0 .../shareit/exception/dto/Violation.java | 0 .../handler/GlobalExceptionHandler.java | 0 .../shareit/item/ItemController.java | 0 .../practicum/shareit/item/ItemService.java | 0 .../shareit/item/ItemServiceImpl.java | 0 .../shareit/item/dto/CommentDto.java | 0 .../practicum/shareit/item/dto/ItemDto.java | 0 .../shareit/item/dto/ItemForRequestDto.java | 0 .../shareit/item/dto/ItemWithBookingDto.java | 0 .../shareit/item/dto/NewCommentDto.java | 0 .../shareit/item/dto/NewItemDto.java | 0 .../shareit/item/dto/UpdateItemDto.java | 0 .../shareit/item/mapper/CommentMapper.java | 0 .../shareit/item/mapper/ItemMapper.java | 0 .../practicum/shareit/item/model/Comment.java | 0 .../ru/practicum/shareit/item/model/Item.java | 0 .../item/repository/CommentRepository.java | 0 .../item/repository/ItemRepository.java | 0 .../request/ItemRequestController.java | 0 .../shareit/request/ItemRequestMapper.java | 0 .../shareit/request/dto/ItemRequestDto.java | 0 .../dto/ItemRequestWithResponsesDto.java | 0 .../request/dto/NewItemRequestDto.java | 0 .../shareit/request/model/ItemRequest.java | 0 .../repository/ItemRequestRepository.java | 0 .../request/service/ItemRequestService.java | 0 .../service/ItemRequestServiceImpl.java | 0 .../shareit/user/UserController.java | 0 .../ru/practicum/shareit/user/UserMapper.java | 0 .../practicum/shareit/user/UserService.java | 0 .../shareit/user/UserServiceImpl.java | 0 .../shareit/user/dto/NewUserDto.java | 0 .../shareit/user/dto/UpdateUserDto.java | 0 .../practicum/shareit/user/dto/UserDto.java | 0 .../ru/practicum/shareit/user/model/User.java | 0 .../user/repository/UserRepository.java | 0 .../resources/application-test.properties | 0 .../main/resources/application.properties | 2 + {src => server/src}/main/resources/schema.sql | 0 .../ru/practicum/shareit/ShareItTests.java | 0 69 files changed, 194 insertions(+), 30 deletions(-) create mode 100644 gateway/Dockerfile create mode 100644 gateway/pom.xml create mode 100644 gateway/src/main/java/ru/practicum/shareit/ShareItGateway.java create mode 100644 gateway/src/main/resources/application.properties create mode 100644 gateway/src/test/java/ru/practicum/shareit/ShareItGatewayTest.java rename Dockerfile => server/Dockerfile (79%) 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 (100%) rename {src => server/src}/main/java/ru/practicum/shareit/booking/BookingMapper.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/booking/BookingService.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/booking/BookingState.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/booking/dto/BookingDto.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/booking/dto/BookingInfoDto.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java (100%) 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/repository/BookingRepository.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/exception/BookingIntersectionException.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/exception/DuplicateDataException.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/exception/ForbiddenAccessException.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/exception/ItemCommentException.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/exception/ItemUnavailableException.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/exception/NotFoundException.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/exception/dto/ErrorResponse.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/exception/dto/Violation.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/ItemController.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/ItemService.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/ItemServiceImpl.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/dto/CommentDto.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/dto/ItemDto.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/dto/ItemForRequestDto.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/dto/ItemWithBookingDto.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/dto/NewCommentDto.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/dto/NewItemDto.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/dto/UpdateItemDto.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/mapper/CommentMapper.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/item/mapper/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 (100%) 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/request/ItemRequestController.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/request/ItemRequestMapper.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/request/dto/ItemRequestWithResponsesDto.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/request/dto/NewItemRequestDto.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/request/model/ItemRequest.java (100%) 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 (100%) rename {src => server/src}/main/java/ru/practicum/shareit/user/UserMapper.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/user/UserService.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/user/UserServiceImpl.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/user/dto/NewUserDto.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java (100%) rename {src => server/src}/main/java/ru/practicum/shareit/user/dto/UserDto.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/resources/application-test.properties (100%) rename {src => server/src}/main/resources/application.properties (96%) rename {src => server/src}/main/resources/schema.sql (100%) rename {src => server/src}/test/java/ru/practicum/shareit/ShareItTests.java (100%) diff --git a/docker-compose.yaml b/docker-compose.yaml index 62c0038..b79678e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -16,19 +16,21 @@ services: timeout: 5s interval: 5s retries: 10 - shareit: - image: shareit:latest - build: . - container_name: shareit-app + server: + image: shareit-server:latest + build: + context: . + dockerfile: server/Dockerfile + container_name: server restart: unless-stopped ports: - - "8080:8080" + - "9090:9090" environment: SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/shareit SPRING_DATASOURCE_USERNAME: shareit_user SPRING_DATASOURCE_PASSWORD: secret healthcheck: - test: wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1 + test: wget --no-verbose --tries=1 --spider http://localhost:9090/actuator/health || exit 1 interval: 30s timeout: 5s start_period: 30s @@ -37,5 +39,25 @@ services: db: condition: service_healthy + gateway: + image: shareit-gateway:latest + build: + context: . + dockerfile: gateway/Dockerfile + container_name: gateway + restart: unless-stopped + ports: + - "8080:8080" + healthcheck: + test: wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1 + interval: 30s + timeout: 5s + start_period: 30s + retries: 5 + depends_on: + server: + condition: service_healthy + + volumes: postgres-data: diff --git a/gateway/Dockerfile b/gateway/Dockerfile new file mode 100644 index 0000000..651c654 --- /dev/null +++ b/gateway/Dockerfile @@ -0,0 +1,40 @@ +FROM maven:3.9.11-amazoncorretto-21 AS builder +WORKDIR /application + +COPY pom.xml ./ + +COPY gateway/pom.xml gateway/pom.xml +COPY server/pom.xml server/pom.xml + +ENV MAVEN_OPTS="-Dmaven.repo.local=/app/.m2/repository" +RUN mvn -pl gateway -am dependency:go-offline -B +COPY gateway/src ./gateway/src + +RUN mvn clean package -pl gateway -am -DskipTests + +FROM amazoncorretto:21.0.8-alpine AS layers +WORKDIR /application +COPY --from=builder /application/gateway/target/*.jar app.jar +RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted + +FROM amazoncorretto:21.0.8-alpine +VOLUME /tmp +RUN adduser -S spring-user +USER spring-user + +WORKDIR /application + +COPY --from=layers /application/extracted/dependencies/ ./ +COPY --from=layers /application/extracted/spring-boot-loader/ ./ +COPY --from=layers /application/extracted/snapshot-dependencies/ ./ +COPY --from=layers /application/extracted/application/ ./ + +RUN java -XX:ArchiveClassesAtExit=app.jsa -Dspring.context.exit=onRefresh -jar app.jar & exit 0 + +ENV JAVA_CDS_OPTS="-XX:SharedArchiveFile=app.jsa -Xlog:class+load:file=/tmp/classload.log" +ENV JAVA_ERROR_FILE_OPTS="-XX:ErrorFile=/tmp/java_error.log" + +ENTRYPOINT java \ + $JAVA_ERROR_FILE_OPTS \ + $JAVA_CDS_OPTS \ + -jar app.jar diff --git a/gateway/pom.xml b/gateway/pom.xml new file mode 100644 index 0000000..e641798 --- /dev/null +++ b/gateway/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + ru.practicum + shareit + 0.0.1-SNAPSHOT + + + shareit-gateway + + ShareIt Gateway + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + 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..f00d25d --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/ShareItGateway.java @@ -0,0 +1,12 @@ +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/resources/application.properties b/gateway/src/main/resources/application.properties new file mode 100644 index 0000000..4c00e40 --- /dev/null +++ b/gateway/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=8080 diff --git a/gateway/src/test/java/ru/practicum/shareit/ShareItGatewayTest.java b/gateway/src/test/java/ru/practicum/shareit/ShareItGatewayTest.java new file mode 100644 index 0000000..2f46fe5 --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/ShareItGatewayTest.java @@ -0,0 +1,13 @@ +package ru.practicum.shareit; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest() +class ShareItGatewayTest { + + @Test + void contextLoads() { + } + +} diff --git a/pom.xml b/pom.xml index 4c9d34f..eafe0bb 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,7 @@ ru.practicum shareit + pom 0.0.1-SNAPSHOT ShareIt @@ -19,6 +20,11 @@ 21 + + gateway + server + + org.springframework.boot @@ -36,35 +42,17 @@ true - - org.springframework.boot - spring-boot-starter-data-jpa - - - - org.postgresql - postgresql - runtime - - org.projectlombok lombok true - - com.h2database - h2 - test - - org.springframework.boot spring-boot-starter-test test - org.springframework.boot spring-boot-starter-validation diff --git a/Dockerfile b/server/Dockerfile similarity index 79% rename from Dockerfile rename to server/Dockerfile index 0d7f86d..90a2921 100644 --- a/Dockerfile +++ b/server/Dockerfile @@ -3,15 +3,18 @@ WORKDIR /application COPY pom.xml ./ +COPY gateway/pom.xml gateway/pom.xml +COPY server/pom.xml server/pom.xml + ENV MAVEN_OPTS="-Dmaven.repo.local=/app/.m2/repository" -RUN mvn dependency:go-offline -B -COPY src ./src +RUN mvn -pl server -am dependency:go-offline -B +COPY server/src ./server/src -RUN mvn clean package -DskipTests +RUN mvn clean package -pl server -am -DskipTests FROM amazoncorretto:21.0.8-alpine AS layers WORKDIR /application -COPY --from=builder /application/target/*.jar app.jar +COPY --from=builder /application/server/target/*.jar app.jar RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted FROM amazoncorretto:21.0.8-alpine diff --git a/server/pom.xml b/server/pom.xml new file mode 100644 index 0000000..559d875 --- /dev/null +++ b/server/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + ru.practicum + shareit + 0.0.1-SNAPSHOT + + + shareit-server + + ShareIt Server + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.postgresql + postgresql + runtime + + + + com.h2database + h2 + test + + + + + + + + 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 a00ad56..303541d 100644 --- a/src/main/java/ru/practicum/shareit/ShareItApp.java +++ b/server/src/main/java/ru/practicum/shareit/ShareItServer.java @@ -4,10 +4,10 @@ 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 100% rename from src/main/java/ru/practicum/shareit/booking/BookingController.java rename to server/src/main/java/ru/practicum/shareit/booking/BookingController.java diff --git a/src/main/java/ru/practicum/shareit/booking/BookingMapper.java b/server/src/main/java/ru/practicum/shareit/booking/BookingMapper.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/BookingMapper.java rename to server/src/main/java/ru/practicum/shareit/booking/BookingMapper.java diff --git a/src/main/java/ru/practicum/shareit/booking/BookingService.java b/server/src/main/java/ru/practicum/shareit/booking/BookingService.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/BookingService.java rename to server/src/main/java/ru/practicum/shareit/booking/BookingService.java diff --git a/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java b/server/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java rename to server/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java diff --git a/src/main/java/ru/practicum/shareit/booking/BookingState.java b/server/src/main/java/ru/practicum/shareit/booking/BookingState.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/BookingState.java rename to server/src/main/java/ru/practicum/shareit/booking/BookingState.java 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 100% 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 diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingInfoDto.java b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingInfoDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/dto/BookingInfoDto.java rename to server/src/main/java/ru/practicum/shareit/booking/dto/BookingInfoDto.java diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java rename to server/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java 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/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/exception/BookingIntersectionException.java b/server/src/main/java/ru/practicum/shareit/exception/BookingIntersectionException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/exception/BookingIntersectionException.java rename to server/src/main/java/ru/practicum/shareit/exception/BookingIntersectionException.java diff --git a/src/main/java/ru/practicum/shareit/exception/DuplicateDataException.java b/server/src/main/java/ru/practicum/shareit/exception/DuplicateDataException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/exception/DuplicateDataException.java rename to server/src/main/java/ru/practicum/shareit/exception/DuplicateDataException.java diff --git a/src/main/java/ru/practicum/shareit/exception/ForbiddenAccessException.java b/server/src/main/java/ru/practicum/shareit/exception/ForbiddenAccessException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/exception/ForbiddenAccessException.java rename to server/src/main/java/ru/practicum/shareit/exception/ForbiddenAccessException.java diff --git a/src/main/java/ru/practicum/shareit/exception/ItemCommentException.java b/server/src/main/java/ru/practicum/shareit/exception/ItemCommentException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/exception/ItemCommentException.java rename to server/src/main/java/ru/practicum/shareit/exception/ItemCommentException.java diff --git a/src/main/java/ru/practicum/shareit/exception/ItemUnavailableException.java b/server/src/main/java/ru/practicum/shareit/exception/ItemUnavailableException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/exception/ItemUnavailableException.java rename to server/src/main/java/ru/practicum/shareit/exception/ItemUnavailableException.java diff --git a/src/main/java/ru/practicum/shareit/exception/NotFoundException.java b/server/src/main/java/ru/practicum/shareit/exception/NotFoundException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/exception/NotFoundException.java rename to server/src/main/java/ru/practicum/shareit/exception/NotFoundException.java diff --git a/src/main/java/ru/practicum/shareit/exception/dto/ErrorResponse.java b/server/src/main/java/ru/practicum/shareit/exception/dto/ErrorResponse.java similarity index 100% rename from src/main/java/ru/practicum/shareit/exception/dto/ErrorResponse.java rename to server/src/main/java/ru/practicum/shareit/exception/dto/ErrorResponse.java diff --git a/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java b/server/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java similarity index 100% rename from src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java rename to server/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java diff --git a/src/main/java/ru/practicum/shareit/exception/dto/Violation.java b/server/src/main/java/ru/practicum/shareit/exception/dto/Violation.java similarity index 100% rename from src/main/java/ru/practicum/shareit/exception/dto/Violation.java rename to server/src/main/java/ru/practicum/shareit/exception/dto/Violation.java diff --git a/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java b/server/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java similarity index 100% rename from src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java rename to server/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java 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 100% rename from src/main/java/ru/practicum/shareit/item/ItemController.java rename to server/src/main/java/ru/practicum/shareit/item/ItemController.java diff --git a/src/main/java/ru/practicum/shareit/item/ItemService.java b/server/src/main/java/ru/practicum/shareit/item/ItemService.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/ItemService.java rename to server/src/main/java/ru/practicum/shareit/item/ItemService.java diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/server/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java rename to server/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java 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 100% 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 diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/dto/ItemDto.java rename to server/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemForRequestDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemForRequestDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/dto/ItemForRequestDto.java rename to server/src/main/java/ru/practicum/shareit/item/dto/ItemForRequestDto.java diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemWithBookingDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemWithBookingDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/dto/ItemWithBookingDto.java rename to server/src/main/java/ru/practicum/shareit/item/dto/ItemWithBookingDto.java diff --git a/src/main/java/ru/practicum/shareit/item/dto/NewCommentDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/NewCommentDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/dto/NewCommentDto.java rename to server/src/main/java/ru/practicum/shareit/item/dto/NewCommentDto.java diff --git a/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java rename to server/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java diff --git a/src/main/java/ru/practicum/shareit/item/dto/UpdateItemDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/UpdateItemDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/dto/UpdateItemDto.java rename to server/src/main/java/ru/practicum/shareit/item/dto/UpdateItemDto.java diff --git a/src/main/java/ru/practicum/shareit/item/mapper/CommentMapper.java b/server/src/main/java/ru/practicum/shareit/item/mapper/CommentMapper.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/mapper/CommentMapper.java rename to server/src/main/java/ru/practicum/shareit/item/mapper/CommentMapper.java diff --git a/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java b/server/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java rename to server/src/main/java/ru/practicum/shareit/item/mapper/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 100% 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 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/request/ItemRequestController.java b/server/src/main/java/ru/practicum/shareit/request/ItemRequestController.java similarity index 100% rename from src/main/java/ru/practicum/shareit/request/ItemRequestController.java rename to server/src/main/java/ru/practicum/shareit/request/ItemRequestController.java diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequestMapper.java b/server/src/main/java/ru/practicum/shareit/request/ItemRequestMapper.java similarity index 100% rename from src/main/java/ru/practicum/shareit/request/ItemRequestMapper.java rename to server/src/main/java/ru/practicum/shareit/request/ItemRequestMapper.java 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 100% 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 diff --git a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestWithResponsesDto.java b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestWithResponsesDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/request/dto/ItemRequestWithResponsesDto.java rename to server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestWithResponsesDto.java diff --git a/src/main/java/ru/practicum/shareit/request/dto/NewItemRequestDto.java b/server/src/main/java/ru/practicum/shareit/request/dto/NewItemRequestDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/request/dto/NewItemRequestDto.java rename to server/src/main/java/ru/practicum/shareit/request/dto/NewItemRequestDto.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 100% 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 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 100% rename from src/main/java/ru/practicum/shareit/user/UserController.java rename to server/src/main/java/ru/practicum/shareit/user/UserController.java diff --git a/src/main/java/ru/practicum/shareit/user/UserMapper.java b/server/src/main/java/ru/practicum/shareit/user/UserMapper.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/UserMapper.java rename to server/src/main/java/ru/practicum/shareit/user/UserMapper.java diff --git a/src/main/java/ru/practicum/shareit/user/UserService.java b/server/src/main/java/ru/practicum/shareit/user/UserService.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/UserService.java rename to server/src/main/java/ru/practicum/shareit/user/UserService.java diff --git a/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java b/server/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/UserServiceImpl.java rename to server/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java diff --git a/src/main/java/ru/practicum/shareit/user/dto/NewUserDto.java b/server/src/main/java/ru/practicum/shareit/user/dto/NewUserDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/dto/NewUserDto.java rename to server/src/main/java/ru/practicum/shareit/user/dto/NewUserDto.java diff --git a/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java b/server/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java rename to server/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java diff --git a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java b/server/src/main/java/ru/practicum/shareit/user/dto/UserDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/dto/UserDto.java rename to server/src/main/java/ru/practicum/shareit/user/dto/UserDto.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/resources/application-test.properties b/server/src/main/resources/application-test.properties similarity index 100% rename from src/main/resources/application-test.properties rename to server/src/main/resources/application-test.properties diff --git a/src/main/resources/application.properties b/server/src/main/resources/application.properties similarity index 96% rename from src/main/resources/application.properties rename to server/src/main/resources/application.properties index 473341c..db48eae 100644 --- a/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -1,3 +1,5 @@ +server.port=9090 + spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.format_sql=true spring.sql.init.mode=always 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/src/test/java/ru/practicum/shareit/ShareItTests.java b/server/src/test/java/ru/practicum/shareit/ShareItTests.java similarity index 100% rename from src/test/java/ru/practicum/shareit/ShareItTests.java rename to server/src/test/java/ru/practicum/shareit/ShareItTests.java From fbf6060a605ee53441d29c248bcff90e55b7fd53 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Wed, 10 Dec 2025 03:15:47 +0300 Subject: [PATCH 05/23] feat: transfer user validation to gateway --- .../shareit/exception/dto/ErrorResponse.java | 4 + .../dto/ValidationErrorResponse.java | 0 .../shareit/exception/dto/Violation.java | 0 .../handler/GlobalExceptionHandler.java | 68 ++++++++++++++++ .../shareit/user/UserController.java | 78 +++++++++++++++++++ .../shareit/user/dto/NewUserDto.java | 11 +++ .../shareit/user/dto/UpdateUserDto.java | 19 +++++ .../handler/GlobalExceptionHandler.java | 36 --------- .../shareit/user/dto/NewUserDto.java | 8 +- .../shareit/user/dto/UpdateUserDto.java | 2 - 10 files changed, 182 insertions(+), 44 deletions(-) create mode 100644 gateway/src/main/java/ru/practicum/shareit/exception/dto/ErrorResponse.java rename {server => gateway}/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java (100%) rename {server => gateway}/src/main/java/ru/practicum/shareit/exception/dto/Violation.java (100%) create mode 100644 gateway/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.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/NewUserDto.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java diff --git a/gateway/src/main/java/ru/practicum/shareit/exception/dto/ErrorResponse.java b/gateway/src/main/java/ru/practicum/shareit/exception/dto/ErrorResponse.java new file mode 100644 index 0000000..4e2e596 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/exception/dto/ErrorResponse.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.exception.dto; + +public record ErrorResponse(String name, String message) { +} diff --git a/server/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java b/gateway/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java similarity index 100% rename from server/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java rename to gateway/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java diff --git a/server/src/main/java/ru/practicum/shareit/exception/dto/Violation.java b/gateway/src/main/java/ru/practicum/shareit/exception/dto/Violation.java similarity index 100% rename from server/src/main/java/ru/practicum/shareit/exception/dto/Violation.java rename to gateway/src/main/java/ru/practicum/shareit/exception/dto/Violation.java diff --git a/gateway/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java b/gateway/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..6ef7044 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java @@ -0,0 +1,68 @@ +package ru.practicum.shareit.exception.handler; + +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +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 org.springframework.web.client.HttpServerErrorException; +import ru.practicum.shareit.exception.dto.ErrorResponse; +import ru.practicum.shareit.exception.dto.ValidationErrorResponse; +import ru.practicum.shareit.exception.dto.Violation; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorResponse onException(Exception ex) { + log.error("Error occurred while processing request {}", ex.getMessage()); + return new ErrorResponse("internal server error", + "An error occurred while processing request"); + } + + @ExceptionHandler(HttpServerErrorException.class) + public ResponseEntity onHttpServerErrorException(HttpServerErrorException ex) { + log.error(ex.getMessage()); + return ResponseEntity.status(ex.getStatusCode()) + .contentType(MediaType.APPLICATION_JSON) + .body(ex.getResponseBodyAsByteArray()); + } + + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ValidationErrorResponse onConstraintValidationException( + ConstraintViolationException ex + ) { + final List violations = ex.getConstraintViolations().stream() + .map( + violation -> new Violation( + violation.getPropertyPath().toString(), + violation.getMessage() + ) + ) + .collect(Collectors.toList()); + log.warn(violations.toString()); + return new ValidationErrorResponse(violations); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ValidationErrorResponse onMethodArgumentNotValidException( + MethodArgumentNotValidException ex + ) { + final List violations = ex.getBindingResult().getFieldErrors().stream() + .map(error -> new Violation(error.getField(), error.getDefaultMessage())) + .collect(Collectors.toList()); + log.warn(violations.toString()); + return new ValidationErrorResponse(violations); + } +} 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..23f3635 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/user/UserController.java @@ -0,0 +1,78 @@ +package ru.practicum.shareit.user; + +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestClient; +import ru.practicum.shareit.user.dto.NewUserDto; +import ru.practicum.shareit.user.dto.UpdateUserDto; + +@Slf4j +@Validated +@RestController +@RequestMapping("/users") +public class UserController { + private final RestClient restClient; + private final String serverUrl; + + public UserController(@Value("${shareit-server.url}") String baseUrl) { + this.restClient = RestClient.create(); + + this.serverUrl = baseUrl.concat("/users"); + } + + @GetMapping + public ResponseEntity getAllUsers() { + log.trace("get all users requested"); + return restClient.get() + .uri(serverUrl) + .retrieve() + .toEntity(Object.class); + } + + @GetMapping("/{userId}") + public ResponseEntity getUser(@PathVariable long userId) { + log.trace("get user requested with id: {}", userId); + return restClient.get() + .uri("%s/%d".formatted(serverUrl, userId)) + .retrieve() + .toEntity(Object.class); + } + + @PostMapping + public ResponseEntity createUser(@RequestBody @Valid NewUserDto newUserDto) { + log.trace("create user requested with body: {}", newUserDto); + return restClient.post() + .uri(serverUrl) + .contentType(MediaType.APPLICATION_JSON) + .body(newUserDto) + .retrieve() + .toEntity(Object.class); + + } + + @PatchMapping("/{userId}") + public ResponseEntity updateUser( + @RequestBody @Valid UpdateUserDto updateUserDto, + @PathVariable long userId + ) { + log.trace("update user requested with id: {} and body {}", userId, updateUserDto); + return restClient.patch() + .uri("%s/%d".formatted(serverUrl, userId)) + .contentType(MediaType.APPLICATION_JSON) + .body(updateUserDto) + .retrieve() + .toEntity(Object.class); + } + + @DeleteMapping("/{userId}") + public void deleteUser(@PathVariable long userId) { + log.trace("delete user requested with id: {}", userId); + restClient.delete() + .uri("%s/%d".formatted(serverUrl, userId)); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/user/dto/NewUserDto.java b/gateway/src/main/java/ru/practicum/shareit/user/dto/NewUserDto.java new file mode 100644 index 0000000..3351658 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/user/dto/NewUserDto.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.user.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record NewUserDto( + @NotBlank String name, + @NotNull @Email String email +) { +} diff --git a/gateway/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java b/gateway/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java new file mode 100644 index 0000000..9609681 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java @@ -0,0 +1,19 @@ +package ru.practicum.shareit.user.dto; + +import jakarta.validation.constraints.Email; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Data +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class UpdateUserDto { + Long id; + String name; + + @Email + String email; + +} diff --git a/server/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java b/server/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java index bb825a9..d0991e1 100644 --- a/server/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java +++ b/server/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java @@ -1,19 +1,12 @@ package ru.practicum.shareit.exception.handler; -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 ru.practicum.shareit.exception.*; import ru.practicum.shareit.exception.dto.ErrorResponse; -import ru.practicum.shareit.exception.dto.ValidationErrorResponse; -import ru.practicum.shareit.exception.dto.Violation; - -import java.util.List; -import java.util.stream.Collectors; @Slf4j @RestControllerAdvice @@ -27,35 +20,6 @@ public ErrorResponse onException(Exception ex) { "An error occurred while processing request"); } - @ExceptionHandler(ConstraintViolationException.class) - @ResponseStatus(HttpStatus.BAD_REQUEST) - public ValidationErrorResponse onConstraintValidationException( - ConstraintViolationException ex - ) { - final List violations = ex.getConstraintViolations().stream() - .map( - violation -> new Violation( - violation.getPropertyPath().toString(), - violation.getMessage() - ) - ) - .collect(Collectors.toList()); - log.warn(violations.toString()); - return new ValidationErrorResponse(violations); - } - - @ExceptionHandler(MethodArgumentNotValidException.class) - @ResponseStatus(HttpStatus.BAD_REQUEST) - public ValidationErrorResponse onMethodArgumentNotValidException( - MethodArgumentNotValidException ex - ) { - final List violations = ex.getBindingResult().getFieldErrors().stream() - .map(error -> new Violation(error.getField(), error.getDefaultMessage())) - .collect(Collectors.toList()); - log.warn(violations.toString()); - return new ValidationErrorResponse(violations); - } - @ExceptionHandler(NotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ErrorResponse onNotFoundException(NotFoundException ex) { diff --git a/server/src/main/java/ru/practicum/shareit/user/dto/NewUserDto.java b/server/src/main/java/ru/practicum/shareit/user/dto/NewUserDto.java index 3351658..e0dff85 100644 --- a/server/src/main/java/ru/practicum/shareit/user/dto/NewUserDto.java +++ b/server/src/main/java/ru/practicum/shareit/user/dto/NewUserDto.java @@ -1,11 +1,7 @@ package ru.practicum.shareit.user.dto; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - public record NewUserDto( - @NotBlank String name, - @NotNull @Email String email + String name, + String email ) { } diff --git a/server/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java b/server/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java index 5bbd748..5eceb3c 100644 --- a/server/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java +++ b/server/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java @@ -1,6 +1,5 @@ package ru.practicum.shareit.user.dto; -import jakarta.validation.constraints.Email; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; @@ -13,7 +12,6 @@ public class UpdateUserDto { Long id; String name; - @Email String email; public boolean hasName() { From 44de83326e8e80096667a12e84ddd776a2543062 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Wed, 10 Dec 2025 03:16:15 +0300 Subject: [PATCH 06/23] feat: add server url as env variable --- docker-compose.yaml | 2 ++ gateway/src/main/resources/application.properties | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index b79678e..2edd805 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -48,6 +48,8 @@ services: restart: unless-stopped ports: - "8080:8080" + environment: + SHAREIT_SERVER_URL: http://server:9090 healthcheck: test: wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1 interval: 30s diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties index 4c00e40..aff1817 100644 --- a/gateway/src/main/resources/application.properties +++ b/gateway/src/main/resources/application.properties @@ -1 +1,3 @@ server.port=8080 + +shareit-server.url=${SHAREIT_SERVER_URL} From 57f58fe1449e20e33a1d46ef9dab929dbb091eb0 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Wed, 10 Dec 2025 04:26:03 +0300 Subject: [PATCH 07/23] feat: remove all validation from server --- pom.xml | 449 +++++++++--------- .../shareit/booking/BookingController.java | 18 +- .../shareit/booking/dto/BookingDto.java | 8 +- .../shareit/item/ItemController.java | 10 +- .../shareit/item/dto/NewCommentDto.java | 4 +- .../shareit/item/dto/NewItemDto.java | 12 +- .../request/ItemRequestController.java | 7 +- .../request/dto/NewItemRequestDto.java | 4 +- .../shareit/user/UserController.java | 7 +- 9 files changed, 246 insertions(+), 273 deletions(-) diff --git a/pom.xml b/pom.xml index eafe0bb..b05ff9a 100644 --- a/pom.xml +++ b/pom.xml @@ -1,244 +1,241 @@ - 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 + ru.practicum + shareit pom - 0.0.1-SNAPSHOT + 0.0.1-SNAPSHOT - ShareIt + ShareIt - - 21 - + + 21 + gateway server - + - - - org.springframework.boot - spring-boot-starter-web - + + + org.springframework.boot + spring-boot-starter-web + - - org.springframework.boot - spring-boot-starter-actuator - + + org.springframework.boot + spring-boot-starter-actuator + - - org.springframework.boot - spring-boot-configuration-processor - true - + + org.springframework.boot + spring-boot-configuration-processor + true + - - org.projectlombok - lombok - true - + + org.projectlombok + lombok + true + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-starter-validation - - + + org.springframework.boot + spring-boot-starter-test + test + - - - - 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 - 10.3 - - - - - 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 + + + + + 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 + 10.3 + + + + + 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/server/src/main/java/ru/practicum/shareit/booking/BookingController.java b/server/src/main/java/ru/practicum/shareit/booking/BookingController.java index e4bc428..c2ba011 100644 --- a/server/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/server/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -1,16 +1,13 @@ package ru.practicum.shareit.booking; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; -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.BookingResponseDto; import java.util.List; -@Validated @RestController @RequiredArgsConstructor @RequestMapping(path = "/bookings") @@ -22,7 +19,7 @@ public class BookingController { @ResponseStatus(HttpStatus.CREATED) public BookingResponseDto createBooking( @RequestHeader(SHARER_USER_ID_HEADER) long bookerId, - @RequestBody @Valid BookingDto bookingDto + @RequestBody BookingDto bookingDto ) { return bookingService.createBooking(bookerId, bookingDto); } @@ -46,21 +43,18 @@ public BookingResponseDto getBooking( @GetMapping public List getAllBookingsOfUser( - @RequestParam(defaultValue = "ALL") String state, + @RequestParam BookingState state, @RequestHeader(SHARER_USER_ID_HEADER) long userId ) { - BookingState bookingState = BookingState.fromString(state) - .orElseThrow(() -> new IllegalArgumentException("Invalid booking state")); - return bookingService.getAllBookingsOfUser(userId, bookingState); + + return bookingService.getAllBookingsOfUser(userId, state); } @GetMapping("/owner") public List getBookingsByOwner( - @RequestParam(defaultValue = "ALL") String state, + @RequestParam BookingState state, @RequestHeader(SHARER_USER_ID_HEADER) long ownerId ) { - BookingState bookingState = BookingState.fromString(state) - .orElseThrow(() -> new IllegalArgumentException("Invalid booking state")); - return bookingService.getAllBookingsByOwner(ownerId, bookingState); + return bookingService.getAllBookingsByOwner(ownerId, state); } } 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 2e091b9..67914b0 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 @@ -1,14 +1,10 @@ package ru.practicum.shareit.booking.dto; -import jakarta.validation.constraints.Future; -import jakarta.validation.constraints.FutureOrPresent; -import jakarta.validation.constraints.NotNull; - import java.time.LocalDateTime; public record BookingDto( long itemId, - @FutureOrPresent @NotNull LocalDateTime start, - @Future @NotNull LocalDateTime end + LocalDateTime start, + LocalDateTime end ) { } diff --git a/server/src/main/java/ru/practicum/shareit/item/ItemController.java b/server/src/main/java/ru/practicum/shareit/item/ItemController.java index 7da600b..bc3cb96 100644 --- a/server/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/server/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -1,17 +1,14 @@ package ru.practicum.shareit.item; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.shareit.item.dto.*; import java.util.Collection; @Slf4j -@Validated @RestController @RequestMapping("/items") @RequiredArgsConstructor @@ -35,9 +32,10 @@ public Collection getItems( } @PostMapping + @ResponseStatus(HttpStatus.CREATED) public ItemDto createItem( @RequestHeader(SHARER_USER_ID_HEADER) long userId, - @RequestBody @Valid NewItemDto newItemDto + @RequestBody NewItemDto newItemDto ) { return itemService.saveItem(userId, newItemDto); } @@ -46,7 +44,7 @@ public ItemDto createItem( public ItemDto updateItem( @RequestHeader(SHARER_USER_ID_HEADER) long userId, @PathVariable long itemId, - @RequestBody @Valid UpdateItemDto updatedItem + @RequestBody UpdateItemDto updatedItem ) { return itemService.updateItem(userId, itemId, updatedItem); } @@ -63,7 +61,7 @@ public Collection searchItems( public CommentDto createComment( @RequestHeader(SHARER_USER_ID_HEADER) long authorId, @PathVariable long itemId, - @RequestBody @Valid NewCommentDto comment + @RequestBody NewCommentDto comment ) { return itemService.createComment(authorId, itemId, comment); } diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/NewCommentDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/NewCommentDto.java index 2eba2c9..fd47bd6 100644 --- a/server/src/main/java/ru/practicum/shareit/item/dto/NewCommentDto.java +++ b/server/src/main/java/ru/practicum/shareit/item/dto/NewCommentDto.java @@ -1,6 +1,4 @@ package ru.practicum.shareit.item.dto; -import jakarta.validation.constraints.NotBlank; - -public record NewCommentDto(@NotBlank String text) { +public record NewCommentDto(String text) { } diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java index a7d9e6c..2b42303 100644 --- a/server/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java +++ b/server/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java @@ -1,13 +1,9 @@ package ru.practicum.shareit.item.dto; -import jakarta.annotation.Nullable; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - public record NewItemDto( - @NotBlank String name, - @NotBlank String description, - @NotNull Boolean available, - @Nullable Long requestId + String name, + String description, + Boolean available, + Long requestId ) { } diff --git a/server/src/main/java/ru/practicum/shareit/request/ItemRequestController.java b/server/src/main/java/ru/practicum/shareit/request/ItemRequestController.java index efb5830..167f548 100644 --- a/server/src/main/java/ru/practicum/shareit/request/ItemRequestController.java +++ b/server/src/main/java/ru/practicum/shareit/request/ItemRequestController.java @@ -1,8 +1,7 @@ package ru.practicum.shareit.request; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.validation.annotation.Validated; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import ru.practicum.shareit.request.dto.ItemRequestDto; import ru.practicum.shareit.request.dto.ItemRequestWithResponsesDto; @@ -11,7 +10,6 @@ import java.util.Collection; -@Validated @RestController @RequiredArgsConstructor @RequestMapping("/requests") @@ -20,9 +18,10 @@ public class ItemRequestController { private final ItemRequestService itemRequestService; @PostMapping + @ResponseStatus(HttpStatus.CREATED) public ItemRequestDto createRequest( @RequestHeader(SHARER_USER_ID_HEADER) long requestorId, - @Valid @RequestBody NewItemRequestDto newItemRequestDto + @RequestBody NewItemRequestDto newItemRequestDto ) { return itemRequestService.create(requestorId, newItemRequestDto); } diff --git a/server/src/main/java/ru/practicum/shareit/request/dto/NewItemRequestDto.java b/server/src/main/java/ru/practicum/shareit/request/dto/NewItemRequestDto.java index 5ec7e6f..67b5527 100644 --- a/server/src/main/java/ru/practicum/shareit/request/dto/NewItemRequestDto.java +++ b/server/src/main/java/ru/practicum/shareit/request/dto/NewItemRequestDto.java @@ -1,8 +1,6 @@ package ru.practicum.shareit.request.dto; -import jakarta.validation.constraints.NotBlank; - public record NewItemRequestDto( - @NotBlank String description + String description ) { } diff --git a/server/src/main/java/ru/practicum/shareit/user/UserController.java b/server/src/main/java/ru/practicum/shareit/user/UserController.java index 8516884..6f15c59 100644 --- a/server/src/main/java/ru/practicum/shareit/user/UserController.java +++ b/server/src/main/java/ru/practicum/shareit/user/UserController.java @@ -1,10 +1,8 @@ package ru.practicum.shareit.user; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.shareit.user.dto.NewUserDto; import ru.practicum.shareit.user.dto.UpdateUserDto; @@ -13,7 +11,6 @@ import java.util.Collection; @Slf4j -@Validated @RestController @RequestMapping("/users") @RequiredArgsConstructor @@ -34,14 +31,14 @@ public UserDto getUser(@PathVariable long userId) { @PostMapping @ResponseStatus(HttpStatus.CREATED) - public UserDto createUser(@RequestBody @Valid NewUserDto newUserDto) { + public UserDto createUser(@RequestBody NewUserDto newUserDto) { log.trace("create user requested with body: {}", newUserDto); return userService.save(newUserDto); } @PatchMapping("/{userId}") public UserDto updateUser( - @RequestBody @Valid UpdateUserDto updateUserDto, + @RequestBody UpdateUserDto updateUserDto, @PathVariable long userId ) { updateUserDto.setId(userId); From b8c3789bf4c89e1aaf749a8445f95f04501f748d Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Wed, 10 Dec 2025 04:26:23 +0300 Subject: [PATCH 08/23] feat: add validation and controllers to gateway --- gateway/pom.xml | 7 ++ .../shareit/booking/BookingController.java | 100 ++++++++++++++++ .../shareit/booking/BookingState.java | 25 ++++ .../shareit/booking/dto/BookingDto.java | 14 +++ .../handler/GlobalExceptionHandler.java | 16 +++ .../shareit/item/ItemController.java | 107 ++++++++++++++++++ .../shareit/item/dto/NewCommentDto.java | 6 + .../shareit/item/dto/NewItemDto.java | 13 +++ .../shareit/item/dto/UpdateItemDto.java | 8 ++ .../request/ItemRequestController.java | 63 +++++++++++ .../request/dto/NewItemRequestDto.java | 8 ++ .../shareit/user/UserController.java | 15 +-- 12 files changed, 373 insertions(+), 9 deletions(-) create mode 100644 gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/booking/BookingState.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.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/NewCommentDto.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java create mode 100644 gateway/src/main/java/ru/practicum/shareit/item/dto/UpdateItemDto.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/NewItemRequestDto.java diff --git a/gateway/pom.xml b/gateway/pom.xml index e641798..c4d9312 100644 --- a/gateway/pom.xml +++ b/gateway/pom.xml @@ -13,6 +13,13 @@ ShareIt Gateway + + + org.springframework.boot + spring-boot-starter-validation + + + 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..7584c49 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -0,0 +1,100 @@ +package ru.practicum.shareit.booking; + +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestClient; +import ru.practicum.shareit.booking.dto.BookingDto; + +@Validated +@RestController +@RequestMapping(path = "/bookings") +public class BookingController { + private static final String SHARER_USER_ID_HEADER = "X-Sharer-User-Id"; + private final RestClient restClient; + + public BookingController(@Value("${shareit-server.url}") String baseUrl) { + this.restClient = RestClient.builder() + .baseUrl(baseUrl.concat("/bookings")) + .build(); + } + + @PostMapping + public ResponseEntity createBooking( + @RequestHeader(SHARER_USER_ID_HEADER) long bookerId, + @RequestBody @Valid BookingDto bookingDto + ) { + return restClient.post() + .contentType(MediaType.APPLICATION_JSON) + .body(bookingDto) + .header(SHARER_USER_ID_HEADER, String.valueOf(bookerId)) + .retrieve() + .toEntity(Object.class); + } + + @PatchMapping("{bookingId}") + public ResponseEntity approveBooking( + @PathVariable long bookingId, + @RequestParam boolean approved, + @RequestHeader(SHARER_USER_ID_HEADER) long ownerId + ) { + return restClient.patch() + .uri(uriBuilder -> uriBuilder + .path("/" + bookingId) + .queryParam("approved", approved) + .build()) + .contentType(MediaType.APPLICATION_JSON) + .header(SHARER_USER_ID_HEADER, String.valueOf(ownerId)) + .retrieve() + .toEntity(Object.class); + } + + @GetMapping("{bookingId}") + public ResponseEntity getBooking( + @PathVariable long bookingId, + @RequestHeader(SHARER_USER_ID_HEADER) long userId + ) { + return restClient.get() + .uri("/" + bookingId) + .header(SHARER_USER_ID_HEADER, String.valueOf(userId)) + .retrieve() + .toEntity(Object.class); + } + + @GetMapping + public ResponseEntity getAllBookingsOfUser( + @RequestParam(defaultValue = "ALL") String state, + @RequestHeader(SHARER_USER_ID_HEADER) long userId + ) { + BookingState bookingState = BookingState.fromString(state) + .orElseThrow(() -> new IllegalArgumentException("Invalid booking state")); + + return restClient.get() + .uri(uriBuilder -> uriBuilder + .queryParam("state", bookingState) + .build()) + .header(SHARER_USER_ID_HEADER, String.valueOf(userId)) + .retrieve() + .toEntity(Object.class); + } + + @GetMapping("/owner") + public ResponseEntity getBookingsByOwner( + @RequestParam(defaultValue = "ALL") String state, + @RequestHeader(SHARER_USER_ID_HEADER) long ownerId + ) { + BookingState bookingState = BookingState.fromString(state) + .orElseThrow(() -> new IllegalArgumentException("Invalid booking state")); + return restClient.get() + .uri(uriBuilder -> uriBuilder + .path("/owner") + .queryParam("state", bookingState) + .build()) + .header(SHARER_USER_ID_HEADER, String.valueOf(ownerId)) + .retrieve() + .toEntity(Object.class); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/BookingState.java b/gateway/src/main/java/ru/practicum/shareit/booking/BookingState.java new file mode 100644 index 0000000..b59f4ff --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/BookingState.java @@ -0,0 +1,25 @@ +package ru.practicum.shareit.booking; + +import java.util.Optional; + +public enum BookingState { + ALL, + CURRENT, + PAST, + FUTURE, + WAITING, + REJECTED; + + public static Optional fromString(String state) { + BookingState bookingState = switch (state.toUpperCase()) { + case "ALL" -> ALL; + case "CURRENT" -> CURRENT; + case "PAST" -> PAST; + case "FUTURE" -> FUTURE; + case "WAITING" -> WAITING; + case "REJECTED" -> REJECTED; + default -> null; + }; + return Optional.ofNullable(bookingState); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java new file mode 100644 index 0000000..2e091b9 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java @@ -0,0 +1,14 @@ +package ru.practicum.shareit.booking.dto; + +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDateTime; + +public record BookingDto( + long itemId, + @FutureOrPresent @NotNull LocalDateTime start, + @Future @NotNull LocalDateTime end +) { +} diff --git a/gateway/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java b/gateway/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java index 6ef7044..59d5a04 100644 --- a/gateway/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java +++ b/gateway/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java @@ -9,6 +9,7 @@ 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.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException; import ru.practicum.shareit.exception.dto.ErrorResponse; import ru.practicum.shareit.exception.dto.ValidationErrorResponse; @@ -37,6 +38,14 @@ public ResponseEntity onHttpServerErrorException(HttpServerErrorExceptio .body(ex.getResponseBodyAsByteArray()); } + @ExceptionHandler(HttpClientErrorException.class) + public ResponseEntity onHttpClientErrorException(HttpClientErrorException ex) { + log.error(ex.getMessage()); + return ResponseEntity.status(ex.getStatusCode()) + .contentType(MediaType.APPLICATION_JSON) + .body(ex.getResponseBodyAsByteArray()); + } + @ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ValidationErrorResponse onConstraintValidationException( @@ -65,4 +74,11 @@ public ValidationErrorResponse onMethodArgumentNotValidException( log.warn(violations.toString()); return new ValidationErrorResponse(violations); } + + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse onIllegalArgumentException(IllegalArgumentException ex) { + log.error(ex.getMessage()); + return new ErrorResponse("illegal argument", ex.getMessage()); + } } 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..0b5be5d --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -0,0 +1,107 @@ +package ru.practicum.shareit.item; + +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestClient; +import ru.practicum.shareit.item.dto.NewCommentDto; +import ru.practicum.shareit.item.dto.NewItemDto; +import ru.practicum.shareit.item.dto.UpdateItemDto; + +@Slf4j +@Validated +@RestController +@RequestMapping("/items") +public class ItemController { + private static final String SHARER_USER_ID_HEADER = "X-Sharer-User-Id"; + private final RestClient restClient; + + public ItemController(@Value("${shareit-server.url}") String baseUrl) { + this.restClient = RestClient.builder() + .baseUrl(baseUrl.concat("/items")) + .build(); + } + + @GetMapping("/{itemId}") + public ResponseEntity getItem( + @RequestHeader(SHARER_USER_ID_HEADER) long userId, + @PathVariable long itemId + ) { + return restClient.get() + .uri("/" + itemId) + .header(SHARER_USER_ID_HEADER, String.valueOf(userId)) + .retrieve() + .toEntity(Object.class); + } + + @GetMapping + public ResponseEntity getItems( + @RequestHeader(SHARER_USER_ID_HEADER) long userId + ) { + return restClient.get() + .header(SHARER_USER_ID_HEADER, String.valueOf(userId)) + .retrieve() + .toEntity(Object.class); + } + + @PostMapping + public ResponseEntity createItem( + @RequestHeader(SHARER_USER_ID_HEADER) long userId, + @RequestBody @Valid NewItemDto newItemDto + ) { + return restClient.post() + .contentType(MediaType.APPLICATION_JSON) + .header(SHARER_USER_ID_HEADER, String.valueOf(userId)) + .body(newItemDto) + .retrieve() + .toEntity(Object.class); + } + + @PatchMapping("/{itemId}") + public ResponseEntity updateItem( + @RequestHeader(SHARER_USER_ID_HEADER) long userId, + @PathVariable long itemId, + @RequestBody @Valid UpdateItemDto updatedItem + ) { + return restClient.patch() + .uri("/" + itemId) + .contentType(MediaType.APPLICATION_JSON) + .header(SHARER_USER_ID_HEADER, String.valueOf(userId)) + .body(updatedItem) + .retrieve() + .toEntity(Object.class); + } + + @GetMapping("/search") + public ResponseEntity searchItems( + @RequestParam(name = "text") String query + ) { + return restClient.get() + .uri(uriBuilder -> uriBuilder + .path("/search") + .queryParam("text", query) + .build()) + .retrieve() + .toEntity(Object.class); + + } + + @PostMapping("/{itemId}/comment") + public ResponseEntity createComment( + @RequestHeader(SHARER_USER_ID_HEADER) long authorId, + @PathVariable long itemId, + @RequestBody @Valid NewCommentDto comment + ) { + return restClient.post() + .uri("/%d/comment".formatted(itemId)) + .contentType(MediaType.APPLICATION_JSON) + .header(SHARER_USER_ID_HEADER, String.valueOf(authorId)) + .body(comment) + .retrieve() + .toEntity(Object.class); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/item/dto/NewCommentDto.java b/gateway/src/main/java/ru/practicum/shareit/item/dto/NewCommentDto.java new file mode 100644 index 0000000..2eba2c9 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/dto/NewCommentDto.java @@ -0,0 +1,6 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.constraints.NotBlank; + +public record NewCommentDto(@NotBlank String text) { +} diff --git a/gateway/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java b/gateway/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java new file mode 100644 index 0000000..a7d9e6c --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java @@ -0,0 +1,13 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record NewItemDto( + @NotBlank String name, + @NotBlank String description, + @NotNull Boolean available, + @Nullable Long requestId +) { +} diff --git a/gateway/src/main/java/ru/practicum/shareit/item/dto/UpdateItemDto.java b/gateway/src/main/java/ru/practicum/shareit/item/dto/UpdateItemDto.java new file mode 100644 index 0000000..de58e41 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/dto/UpdateItemDto.java @@ -0,0 +1,8 @@ +package ru.practicum.shareit.item.dto; + +public record UpdateItemDto( + String name, + String description, + Boolean available +) { +} 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..678ecb7 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/request/ItemRequestController.java @@ -0,0 +1,63 @@ +package ru.practicum.shareit.request; + +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestClient; +import ru.practicum.shareit.request.dto.NewItemRequestDto; + +@Validated +@RestController +@RequestMapping("/requests") +public class ItemRequestController { + private static final String SHARER_USER_ID_HEADER = "X-Sharer-User-Id"; + private final RestClient restClient; + private final String serverUrl; + + public ItemRequestController(@Value("${shareit-server.url}") String baseUrl) { + this.restClient = RestClient.create(); + this.serverUrl = baseUrl.concat("/requests"); + } + + @PostMapping + public ResponseEntity createRequest( + @RequestHeader(SHARER_USER_ID_HEADER) long requestorId, + @Valid @RequestBody NewItemRequestDto newItemRequestDto + ) { + return restClient.post() + .uri(serverUrl) + .contentType(MediaType.APPLICATION_JSON) + .body(newItemRequestDto) + .header(SHARER_USER_ID_HEADER, String.valueOf(requestorId)) + .retrieve() + .toEntity(Object.class); + } + + @GetMapping + public ResponseEntity getAllRequestsOfUser(@RequestHeader(SHARER_USER_ID_HEADER) long userId) { + return restClient.get() + .uri(serverUrl) + .header(SHARER_USER_ID_HEADER, String.valueOf(userId)) + .retrieve() + .toEntity(Object.class); + } + + @GetMapping("/all") + public ResponseEntity getAllRequests() { + return restClient.get() + .uri("%s/all".formatted(serverUrl)) + .retrieve() + .toEntity(Object.class); + } + + @GetMapping("/{requestId}") + public ResponseEntity getRequestById(@PathVariable long requestId) { + return restClient.get() + .uri("%s/%d".formatted(serverUrl, requestId)) + .retrieve() + .toEntity(Object.class); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/request/dto/NewItemRequestDto.java b/gateway/src/main/java/ru/practicum/shareit/request/dto/NewItemRequestDto.java new file mode 100644 index 0000000..5ec7e6f --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/request/dto/NewItemRequestDto.java @@ -0,0 +1,8 @@ +package ru.practicum.shareit.request.dto; + +import jakarta.validation.constraints.NotBlank; + +public record NewItemRequestDto( + @NotBlank String description +) { +} diff --git a/gateway/src/main/java/ru/practicum/shareit/user/UserController.java b/gateway/src/main/java/ru/practicum/shareit/user/UserController.java index 23f3635..7bba90b 100644 --- a/gateway/src/main/java/ru/practicum/shareit/user/UserController.java +++ b/gateway/src/main/java/ru/practicum/shareit/user/UserController.java @@ -17,19 +17,17 @@ @RequestMapping("/users") public class UserController { private final RestClient restClient; - private final String serverUrl; public UserController(@Value("${shareit-server.url}") String baseUrl) { - this.restClient = RestClient.create(); - - this.serverUrl = baseUrl.concat("/users"); + this.restClient = RestClient.builder() + .baseUrl(baseUrl.concat("/users")) + .build(); } @GetMapping public ResponseEntity getAllUsers() { log.trace("get all users requested"); return restClient.get() - .uri(serverUrl) .retrieve() .toEntity(Object.class); } @@ -38,7 +36,7 @@ public ResponseEntity getAllUsers() { public ResponseEntity getUser(@PathVariable long userId) { log.trace("get user requested with id: {}", userId); return restClient.get() - .uri("%s/%d".formatted(serverUrl, userId)) + .uri("/" + userId) .retrieve() .toEntity(Object.class); } @@ -47,7 +45,6 @@ public ResponseEntity getUser(@PathVariable long userId) { public ResponseEntity createUser(@RequestBody @Valid NewUserDto newUserDto) { log.trace("create user requested with body: {}", newUserDto); return restClient.post() - .uri(serverUrl) .contentType(MediaType.APPLICATION_JSON) .body(newUserDto) .retrieve() @@ -62,7 +59,7 @@ public ResponseEntity updateUser( ) { log.trace("update user requested with id: {} and body {}", userId, updateUserDto); return restClient.patch() - .uri("%s/%d".formatted(serverUrl, userId)) + .uri("/" + userId) .contentType(MediaType.APPLICATION_JSON) .body(updateUserDto) .retrieve() @@ -73,6 +70,6 @@ public ResponseEntity updateUser( public void deleteUser(@PathVariable long userId) { log.trace("delete user requested with id: {}", userId); restClient.delete() - .uri("%s/%d".formatted(serverUrl, userId)); + .uri("/" + userId); } } From 9b4f0a1f52ac434a4b0fe86c9dd191674ea82d49 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Wed, 10 Dec 2025 04:52:11 +0300 Subject: [PATCH 09/23] feat: add json tests for gateway --- .../booking/dto/BookingDtoJsonTest.java | 83 +++++++++++++++++++ .../item/dto/NewCommentDtoJsonTest.java | 51 ++++++++++++ .../shareit/item/dto/NewItemDtoJsonTest.java | 67 +++++++++++++++ .../item/dto/NewItemRequestDtoJsonTest.java | 34 ++++++++ .../shareit/user/dto/NewUserDtoJsonTest.java | 62 ++++++++++++++ .../user/dto/UpdateUserDtoJsonTest.java | 46 ++++++++++ .../resources/application-test.properties | 1 + 7 files changed, 344 insertions(+) create mode 100644 gateway/src/test/java/ru/practicum/shareit/booking/dto/BookingDtoJsonTest.java create mode 100644 gateway/src/test/java/ru/practicum/shareit/item/dto/NewCommentDtoJsonTest.java create mode 100644 gateway/src/test/java/ru/practicum/shareit/item/dto/NewItemDtoJsonTest.java create mode 100644 gateway/src/test/java/ru/practicum/shareit/item/dto/NewItemRequestDtoJsonTest.java create mode 100644 gateway/src/test/java/ru/practicum/shareit/user/dto/NewUserDtoJsonTest.java create mode 100644 gateway/src/test/java/ru/practicum/shareit/user/dto/UpdateUserDtoJsonTest.java create mode 100644 gateway/src/test/resources/application-test.properties diff --git a/gateway/src/test/java/ru/practicum/shareit/booking/dto/BookingDtoJsonTest.java b/gateway/src/test/java/ru/practicum/shareit/booking/dto/BookingDtoJsonTest.java new file mode 100644 index 0000000..0ddc0c9 --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/booking/dto/BookingDtoJsonTest.java @@ -0,0 +1,83 @@ +package ru.practicum.shareit.booking.dto; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +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 java.time.LocalDateTime; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +class BookingDtoJsonTest { + + private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + @Autowired + private JacksonTester json; + + @Test + void shouldDeserializeValidBookingDto() throws Exception { + String content = """ + { + "itemId": 1, + "start": "%s", + "end": "%s" + } + """.formatted( + LocalDateTime.now().plusDays(1), + LocalDateTime.now().plusDays(2) + ); + + BookingDto dto = json.parseObject(content); + + assertThat(dto.itemId()).isEqualTo(1); + assertThat(validator.validate(dto)).isEmpty(); + } + + @Test + void shouldFailValidationWhenStartInPast() throws Exception { + String content = """ + { + "itemId": 1, + "start": "%s", + "end": "%s" + } + """.formatted( + LocalDateTime.now().minusDays(1), + LocalDateTime.now().plusDays(1) + ); + + BookingDto dto = json.parseObject(content); + + Set> violations = validator.validate(dto); + + assertThat(violations) + .anyMatch(v -> v.getPropertyPath().toString().equals("start")); + } + + @Test + void shouldFailValidationWhenEndNotInFuture() throws Exception { + String content = """ + { + "itemId": 1, + "start": "%s", + "end": "%s" + } + """.formatted( + LocalDateTime.now().plusDays(1), + LocalDateTime.now() + ); + + BookingDto dto = json.parseObject(content); + + Set> violations = validator.validate(dto); + + assertThat(violations) + .anyMatch(v -> v.getPropertyPath().toString().equals("end")); + } +} diff --git a/gateway/src/test/java/ru/practicum/shareit/item/dto/NewCommentDtoJsonTest.java b/gateway/src/test/java/ru/practicum/shareit/item/dto/NewCommentDtoJsonTest.java new file mode 100644 index 0000000..c56712e --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/item/dto/NewCommentDtoJsonTest.java @@ -0,0 +1,51 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +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 java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +class NewCommentDtoJsonTest { + + @Autowired + private JacksonTester json; + + private Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + + @Test + void shouldDeserializeValidComment() throws Exception { + String content = """ + { + "text": "Отличная вещь!" + } + """; + + NewCommentDto dto = json.parseObject(content); + + assertThat(validator.validate(dto)).isEmpty(); + } + + @Test + void shouldFailValidationWhenTextIsBlank() throws Exception { + String content = """ + { + "text": " " + } + """; + + NewCommentDto dto = json.parseObject(content); + + Set> violations = validator.validate(dto); + + assertThat(violations) + .anyMatch(v -> v.getPropertyPath().toString().equals("text")); + } +} diff --git a/gateway/src/test/java/ru/practicum/shareit/item/dto/NewItemDtoJsonTest.java b/gateway/src/test/java/ru/practicum/shareit/item/dto/NewItemDtoJsonTest.java new file mode 100644 index 0000000..4419529 --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/item/dto/NewItemDtoJsonTest.java @@ -0,0 +1,67 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +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 static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +class NewItemDtoJsonTest { + + @Autowired + private JacksonTester json; + + private Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + + @Test + void shouldDeserializeValidItem() throws Exception { + String content = """ + { + "name": "Дрель", + "description": "Мощная дрель", + "available": true, + "requestId": null + } + """; + + NewItemDto dto = json.parseObject(content); + + assertThat(validator.validate(dto)).isEmpty(); + } + + @Test + void shouldFailValidationWhenNameIsBlank() throws Exception { + String content = """ + { + "name": " ", + "description": "Описание", + "available": true + } + """; + + NewItemDto dto = json.parseObject(content); + + assertThat(validator.validate(dto)) + .anyMatch(v -> v.getPropertyPath().toString().equals("name")); + } + + @Test + void shouldFailValidationWhenAvailableIsNull() throws Exception { + String content = """ + { + "name": "Дрель", + "description": "Описание", + "available": null + } + """; + + NewItemDto dto = json.parseObject(content); + + assertThat(validator.validate(dto)) + .anyMatch(v -> v.getPropertyPath().toString().equals("available")); + } +} diff --git a/gateway/src/test/java/ru/practicum/shareit/item/dto/NewItemRequestDtoJsonTest.java b/gateway/src/test/java/ru/practicum/shareit/item/dto/NewItemRequestDtoJsonTest.java new file mode 100644 index 0000000..0367f7a --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/item/dto/NewItemRequestDtoJsonTest.java @@ -0,0 +1,34 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +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 ru.practicum.shareit.request.dto.NewItemRequestDto; + +import static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +class NewItemRequestDtoJsonTest { + + @Autowired + private JacksonTester json; + + private Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + + @Test + void shouldFailValidationWhenDescriptionIsBlank() throws Exception { + String content = """ + { + "description": "" + } + """; + + NewItemRequestDto dto = json.parseObject(content); + + assertThat(validator.validate(dto)) + .anyMatch(v -> v.getPropertyPath().toString().equals("description")); + } +} diff --git a/gateway/src/test/java/ru/practicum/shareit/user/dto/NewUserDtoJsonTest.java b/gateway/src/test/java/ru/practicum/shareit/user/dto/NewUserDtoJsonTest.java new file mode 100644 index 0000000..011ac1e --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/user/dto/NewUserDtoJsonTest.java @@ -0,0 +1,62 @@ +package ru.practicum.shareit.user.dto; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +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 static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +class NewUserDtoJsonTest { + + @Autowired + private JacksonTester json; + + private Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + + @Test + void shouldDeserializeValidUser() throws Exception { + String content = """ + { + "name": "Иван", + "email": "ivan@test.com" + } + """; + + NewUserDto dto = json.parseObject(content); + + assertThat(validator.validate(dto)).isEmpty(); + } + + @Test + void shouldFailValidationWhenEmailInvalid() throws Exception { + String content = """ + { + "name": "Иван", + "email": "not-email" + } + """; + + NewUserDto dto = json.parseObject(content); + + assertThat(validator.validate(dto)) + .anyMatch(v -> v.getPropertyPath().toString().equals("email")); + } + + @Test + void shouldFailValidationWhenEmailIsNull() throws Exception { + String content = """ + { + "name": "Иван" + } + """; + + NewUserDto dto = json.parseObject(content); + + assertThat(validator.validate(dto)) + .anyMatch(v -> v.getPropertyPath().toString().equals("email")); + } +} diff --git a/gateway/src/test/java/ru/practicum/shareit/user/dto/UpdateUserDtoJsonTest.java b/gateway/src/test/java/ru/practicum/shareit/user/dto/UpdateUserDtoJsonTest.java new file mode 100644 index 0000000..45ff8bf --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/user/dto/UpdateUserDtoJsonTest.java @@ -0,0 +1,46 @@ +package ru.practicum.shareit.user.dto; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +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 static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +class UpdateUserDtoJsonTest { + + @Autowired + private JacksonTester json; + + private Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + + @Test + void shouldDeserializeWithOnlyName() throws Exception { + String content = """ + { + "name": "Новое имя" + } + """; + + UpdateUserDto dto = json.parseObject(content); + + assertThat(validator.validate(dto)).isEmpty(); + } + + @Test + void shouldFailValidationWhenEmailInvalid() throws Exception { + String content = """ + { + "email": "wrong-email" + } + """; + + UpdateUserDto dto = json.parseObject(content); + + assertThat(validator.validate(dto)) + .anyMatch(v -> v.getPropertyPath().toString().equals("email")); + } +} diff --git a/gateway/src/test/resources/application-test.properties b/gateway/src/test/resources/application-test.properties new file mode 100644 index 0000000..1221107 --- /dev/null +++ b/gateway/src/test/resources/application-test.properties @@ -0,0 +1 @@ +shareit-server.url=http://localhost:9090 From 1080480e66bfda49d1687291c7c696c1d943362e Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Wed, 10 Dec 2025 05:30:20 +0300 Subject: [PATCH 10/23] feat: add controller tests --- .../exception/GlobalExceptionHandlerTest.java | 111 +++++++++++ .../shareit/user/dto/UpdateUserDto.java | 2 + .../booking/BookingControllerTest.java | 162 +++++++++++++++ .../shareit/item/ItemControllerTest.java | 186 ++++++++++++++++++ .../request/ItemRequestControllerTest.java | 128 ++++++++++++ .../shareit/user/UserControllerTest.java | 105 ++++++++++ 6 files changed, 694 insertions(+) create mode 100644 gateway/src/test/java/ru/practicum/shareit/exception/GlobalExceptionHandlerTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/item/ItemControllerTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/request/ItemRequestControllerTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/user/UserControllerTest.java diff --git a/gateway/src/test/java/ru/practicum/shareit/exception/GlobalExceptionHandlerTest.java b/gateway/src/test/java/ru/practicum/shareit/exception/GlobalExceptionHandlerTest.java new file mode 100644 index 0000000..cd7b20f --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/exception/GlobalExceptionHandlerTest.java @@ -0,0 +1,111 @@ +package ru.practicum.shareit.exception; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Path; +import jakarta.validation.metadata.ConstraintDescriptor; +import org.hibernate.validator.internal.engine.path.PathImpl; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.exception.dto.ErrorResponse; +import ru.practicum.shareit.exception.dto.ValidationErrorResponse; +import ru.practicum.shareit.exception.dto.Violation; +import ru.practicum.shareit.exception.handler.GlobalExceptionHandler; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class GlobalExceptionHandlerUnitTest { + + private final GlobalExceptionHandler handler = new GlobalExceptionHandler(); + + @Test + @DisplayName("Handle IllegalArgumentException") + void handleIllegalArgumentException() { + IllegalArgumentException ex = new IllegalArgumentException("illegal argument"); + ErrorResponse response = handler.onIllegalArgumentException(ex); + + assertEquals("illegal argument", response.name()); + assertEquals("illegal argument", response.message()); + } + + @Test + @DisplayName("Handle ConstraintViolationException") + void handleConstraintViolationException() { + ConstraintViolation violation = new ConstraintViolation<>() { + @Override + public String getMessage() { + return "must not be blank"; + } + + @Override + public String getMessageTemplate() { + return null; + } + + @Override + public String getRootBean() { + return null; + } + + @Override + public Class getRootBeanClass() { + return String.class; + } + + @Override + public Object getLeafBean() { + return null; + } + + @Override + public Object[] getExecutableParameters() { + return new Object[0]; + } + + @Override + public Object getExecutableReturnValue() { + return null; + } + + @Override + public Path getPropertyPath() { + return PathImpl.createPathFromString("field"); + } + + @Override + public Object getInvalidValue() { + return null; + } + + @Override + public ConstraintDescriptor getConstraintDescriptor() { + return null; + } + + @Override + public U unwrap(Class type) { + return null; + } + }; + + ConstraintViolationException ex = new ConstraintViolationException(Set.of(violation)); + ValidationErrorResponse response = handler.onConstraintValidationException(ex); + + assertEquals(1, response.error().size()); + Violation v = response.error().getFirst(); + assertEquals("field", v.fieldName()); + assertEquals("must not be blank", v.message()); + } + + @Test + @DisplayName("Handle generic Exception") + void handleGenericException() { + Exception ex = new RuntimeException("runtime error"); + var response = handler.onException(ex); + + assertEquals("internal server error", response.name()); + assertEquals("An error occurred while processing request", response.message()); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java b/server/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java index 5eceb3c..358c576 100644 --- a/server/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java +++ b/server/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java @@ -3,10 +3,12 @@ import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; @Data @AllArgsConstructor +@NoArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) public class UpdateUserDto { Long id; 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..e1a1946 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java @@ -0,0 +1,162 @@ +package ru.practicum.shareit.booking; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; +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; +import java.util.List; + +import static org.mockito.ArgumentMatchers.*; +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; + +@WebMvcTest(BookingController.class) +class BookingControllerTest { + + private static final String HEADER_USER_ID = "X-Sharer-User-Id"; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private BookingService bookingService; + + @Test + @DisplayName("Create a new booking via POST /bookings") + void createBookingTest() throws Exception { + BookingDto bookingDto = new BookingDto( + 1L, + LocalDateTime.now().plusDays(1), + LocalDateTime.now().plusDays(2) + ); + + BookingResponseDto responseDto = new BookingResponseDto( + 1L, + bookingDto.start(), + bookingDto.end(), + new ItemDto(1L, "ItemName", "ItemDescription", true), + new UserDto(2L, "John", "mail@mail"), + BookingStatus.WAITING + ); + + Mockito.when(bookingService.createBooking(anyLong(), any(BookingDto.class))) + .thenReturn(responseDto); + + mockMvc.perform(post("/bookings") + .header(HEADER_USER_ID, 2L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(bookingDto))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.item.id").value(1L)) + .andExpect(jsonPath("$.booker.id").value(2L)) + .andExpect(jsonPath("$.status").value("WAITING")); + } + + @Test + @DisplayName("Approve a booking via PATCH /bookings/{id}") + void approveBookingTest() throws Exception { + BookingResponseDto responseDto = new BookingResponseDto( + 1L, + LocalDateTime.now().plusDays(1), + LocalDateTime.now().plusDays(2), + new ItemDto(1L, "ItemName", "ItemDescription", true), + new UserDto(2L, "John", "mail@mail"), + BookingStatus.APPROVED + ); + + Mockito.when(bookingService.approveBooking(anyLong(), anyBoolean(), anyLong())) + .thenReturn(responseDto); + + mockMvc.perform(patch("/bookings/{id}", 1) + .param("approved", "true") + .header(HEADER_USER_ID, 2L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.status").value("APPROVED")); + } + + @Test + @DisplayName("Get booking by ID via GET /bookings/{id}") + void getBookingByIdTest() throws Exception { + BookingResponseDto responseDto = new BookingResponseDto( + 1L, + LocalDateTime.now().plusDays(1), + LocalDateTime.now().plusDays(2), + new ItemDto(1L, "ItemName", "ItemDescription", true), + new UserDto(2L, "John", "mail@mail"), + BookingStatus.WAITING + ); + + Mockito.when(bookingService.getBookingById(anyLong(), anyLong())) + .thenReturn(responseDto); + + mockMvc.perform(get("/bookings/{id}", 1) + .header(HEADER_USER_ID, 2L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.booker.id").value(2L)); + } + + @Test + @DisplayName("Get all bookings of user via GET /bookings") + void getAllBookingsOfUserTest() throws Exception { + BookingResponseDto responseDto = new BookingResponseDto( + 1L, + LocalDateTime.now().plusDays(1), + LocalDateTime.now().plusDays(2), + new ItemDto(1L, "ItemName", "ItemDescription", true), + new UserDto(2L, "John", "email@email"), + BookingStatus.WAITING + ); + + Mockito.when(bookingService.getAllBookingsOfUser(anyLong(), any())) + .thenReturn(List.of(responseDto)); + + mockMvc.perform(get("/bookings") + .param("state", "ALL") + .header(HEADER_USER_ID, 2L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1L)) + .andExpect(jsonPath("$[0].booker.id").value(2L)); + } + + @Test + @DisplayName("Get all bookings by owner via GET /bookings/owner") + void getBookingsByOwnerTest() throws Exception { + BookingResponseDto responseDto = new BookingResponseDto( + 1L, + LocalDateTime.now().plusDays(1), + LocalDateTime.now().plusDays(2), + new ItemDto(1L, "ItemName", "ItemDescription", true), + new UserDto(2L, "John", "email@email"), + BookingStatus.WAITING + ); + + Mockito.when(bookingService.getAllBookingsByOwner(anyLong(), any())) + .thenReturn(List.of(responseDto)); + + mockMvc.perform(get("/bookings/owner") + .param("state", "ALL") + .header(HEADER_USER_ID, 2L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1L)) + .andExpect(jsonPath("$[0].booker.id").value(2L)); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemControllerTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemControllerTest.java new file mode 100644 index 0000000..80c46d5 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/ItemControllerTest.java @@ -0,0 +1,186 @@ +package ru.practicum.shareit.item; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.booking.dto.BookingInfoDto; +import ru.practicum.shareit.item.dto.*; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.mockito.ArgumentMatchers.*; +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; + +@WebMvcTest(ItemController.class) +class ItemControllerTest { + + private static final String HEADER_USER_ID = "X-Sharer-User-Id"; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private ItemService itemService; + + @Test + @DisplayName("Get item by ID via GET /items/{itemId}") + void getItemTest() throws Exception { + ItemWithBookingDto responseDto = new ItemWithBookingDto( + 1L, + "ItemName", + "ItemDescription", + true, + new BookingInfoDto(10L, 2L, LocalDateTime.now().minusDays(1), LocalDateTime.now()), + new BookingInfoDto(11L, 3L, LocalDateTime.now().plusDays(1), LocalDateTime.now().plusDays(2)), + List.of(new CommentDto(1L, "Nice item!", "Alice", LocalDateTime.now())) + ); + + Mockito.when(itemService.getItemOfUserById(anyLong(), anyLong())) + .thenReturn(responseDto); + + mockMvc.perform(get("/items/{itemId}", 1) + .header(HEADER_USER_ID, 2L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.name").value("ItemName")) + .andExpect(jsonPath("$.lastBooking.id").value(10L)) + .andExpect(jsonPath("$.nextBooking.id").value(11L)) + .andExpect(jsonPath("$.comments[0].text").value("Nice item!")); + } + + @Test + @DisplayName("Get all items of user via GET /items") + void getItemsTest() throws Exception { + ItemWithBookingDto itemDto = new ItemWithBookingDto( + 1L, + "ItemName", + "ItemDescription", + true, + null, + null, + List.of() + ); + + Mockito.when(itemService.getAllItemsOfUser(anyLong())) + .thenReturn(List.of(itemDto)); + + mockMvc.perform(get("/items") + .header(HEADER_USER_ID, 2L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1L)) + .andExpect(jsonPath("$[0].name").value("ItemName")); + } + + @Test + @DisplayName("Create a new item via POST /items") + void createItemTest() throws Exception { + NewItemDto newItemDto = new NewItemDto( + "ItemName", + "ItemDescription", + true, + null + ); + + ItemDto responseDto = new ItemDto( + 1L, + "ItemName", + "ItemDescription", + true + ); + + Mockito.when(itemService.saveItem(anyLong(), any(NewItemDto.class))) + .thenReturn(responseDto); + + mockMvc.perform(post("/items") + .header(HEADER_USER_ID, 2L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(newItemDto))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.name").value("ItemName")); + } + + @Test + @DisplayName("Update item via PATCH /items/{itemId}") + void updateItemTest() throws Exception { + UpdateItemDto updatedItem = new UpdateItemDto( + "UpdatedName", + "UpdatedDescription", + true + ); + + ItemDto responseDto = new ItemDto( + 1L, + "UpdatedName", + "UpdatedDescription", + true + ); + + Mockito.when(itemService.updateItem(anyLong(), anyLong(), any(UpdateItemDto.class))) + .thenReturn(responseDto); + + mockMvc.perform(patch("/items/{itemId}", 1) + .header(HEADER_USER_ID, 2L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updatedItem))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("UpdatedName")) + .andExpect(jsonPath("$.description").value("UpdatedDescription")); + } + + @Test + @DisplayName("Search items via GET /items/search") + void searchItemsTest() throws Exception { + ItemDto itemDto = new ItemDto( + 1L, + "ItemName", + "ItemDescription", + true + ); + + Mockito.when(itemService.searchItems(anyString())) + .thenReturn(List.of(itemDto)); + + mockMvc.perform(get("/items/search") + .param("text", "ItemName")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1L)) + .andExpect(jsonPath("$[0].name").value("ItemName")); + } + + @Test + @DisplayName("Create a comment via POST /items/{itemId}/comment") + void createCommentTest() throws Exception { + NewCommentDto newComment = new NewCommentDto("Great item!"); + + CommentDto responseDto = new CommentDto( + 1L, + "Great item!", + "John", + LocalDateTime.now() + ); + + Mockito.when(itemService.createComment(anyLong(), anyLong(), any(NewCommentDto.class))) + .thenReturn(responseDto); + + mockMvc.perform(post("/items/{itemId}/comment", 1) + .header(HEADER_USER_ID, 2L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(newComment))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.text").value("Great item!")); + } +} 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..1e122c2 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/ItemRequestControllerTest.java @@ -0,0 +1,128 @@ +package ru.practicum.shareit.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.item.dto.ItemForRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestWithResponsesDto; +import ru.practicum.shareit.request.dto.NewItemRequestDto; +import ru.practicum.shareit.request.service.ItemRequestService; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.mockito.ArgumentMatchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(ItemRequestController.class) +class ItemRequestControllerTest { + + private static final String HEADER_USER_ID = "X-Sharer-User-Id"; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private ItemRequestService itemRequestService; + + @Test + @DisplayName("Create a new item request via POST /requests") + void createRequestTest() throws Exception { + NewItemRequestDto newRequestDto = new NewItemRequestDto("Need a drill"); + + ItemRequestDto responseDto = new ItemRequestDto( + 1L, + "Need a drill", + 2L, + LocalDateTime.now() + ); + + Mockito.when(itemRequestService.create(anyLong(), any(NewItemRequestDto.class))) + .thenReturn(responseDto); + + mockMvc.perform(post("/requests") + .header(HEADER_USER_ID, 2L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(newRequestDto))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.description").value("Need a drill")) + .andExpect(jsonPath("$.requestorId").value(2L)); + } + + @Test + @DisplayName("Get all item requests of user via GET /requests") + void getAllRequestsOfUserTest() throws Exception { + ItemRequestWithResponsesDto responseDto = new ItemRequestWithResponsesDto( + 1L, + "Need a drill", + LocalDateTime.now(), + List.of(new ItemForRequestDto(10L, "Drill", 5L)) + ); + + Mockito.when(itemRequestService.getAllOfUser(anyLong())) + .thenReturn(List.of(responseDto)); + + mockMvc.perform(get("/requests") + .header(HEADER_USER_ID, 2L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1L)) + .andExpect(jsonPath("$[0].description").value("Need a drill")) + .andExpect(jsonPath("$[0].items[0].id").value(10L)) + .andExpect(jsonPath("$[0].items[0].name").value("Drill")) + .andExpect(jsonPath("$[0].items[0].ownerId").value(5L)); + } + + @Test + @DisplayName("Get all item requests via GET /requests/all") + void getAllRequestsTest() throws Exception { + ItemRequestDto responseDto = new ItemRequestDto( + 1L, + "Need a drill", + 2L, + LocalDateTime.now() + ); + + Mockito.when(itemRequestService.getAll()) + .thenReturn(List.of(responseDto)); + + mockMvc.perform(get("/requests/all")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1L)) + .andExpect(jsonPath("$[0].description").value("Need a drill")) + .andExpect(jsonPath("$[0].requestorId").value(2L)); + } + + @Test + @DisplayName("Get item request by ID via GET /requests/{requestId}") + void getRequestByIdTest() throws Exception { + ItemRequestWithResponsesDto responseDto = new ItemRequestWithResponsesDto( + 1L, + "Need a drill", + LocalDateTime.now(), + List.of(new ItemForRequestDto(10L, "Drill", 5L)) + ); + + Mockito.when(itemRequestService.getById(anyLong())) + .thenReturn(responseDto); + + mockMvc.perform(get("/requests/{requestId}", 1)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.description").value("Need a drill")) + .andExpect(jsonPath("$.items[0].id").value(10L)) + .andExpect(jsonPath("$.items[0].name").value("Drill")) + .andExpect(jsonPath("$.items[0].ownerId").value(5L)); + } +} 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..5b468c0 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserControllerTest.java @@ -0,0 +1,105 @@ +package ru.practicum.shareit.user; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.user.dto.NewUserDto; +import ru.practicum.shareit.user.dto.UpdateUserDto; +import ru.practicum.shareit.user.dto.UserDto; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(UserController.class) +class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private UserService userService; + + @Test + @DisplayName("Get all users via GET /users") + void getAllUsersTest() throws Exception { + UserDto user = new UserDto(1L, "John", "john@example.com"); + Mockito.when(userService.findAll()).thenReturn(List.of(user)); + + mockMvc.perform(get("/users")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1L)) + .andExpect(jsonPath("$[0].name").value("John")) + .andExpect(jsonPath("$[0].email").value("john@example.com")); + } + + @Test + @DisplayName("Get user by ID via GET /users/{id}") + void getUserByIdTest() throws Exception { + UserDto user = new UserDto(1L, "John", "john@example.com"); + Mockito.when(userService.findById(anyLong())).thenReturn(user); + + mockMvc.perform(get("/users/{id}", 1)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.name").value("John")) + .andExpect(jsonPath("$.email").value("john@example.com")); + } + + @Test + @DisplayName("Create a new user via POST /users") + void createUserTest() throws Exception { + NewUserDto newUser = new NewUserDto("John", "john@example.com"); + UserDto savedUser = new UserDto(1L, "John", "john@example.com"); + + Mockito.when(userService.save(any(NewUserDto.class))).thenReturn(savedUser); + + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(newUser))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.name").value("John")) + .andExpect(jsonPath("$.email").value("john@example.com")); + } + + @Test + @DisplayName("Update user via PATCH /users/{id}") + void updateUserTest() throws Exception { + UpdateUserDto updateUser = new UpdateUserDto(); + updateUser.setName("Johnny"); + updateUser.setEmail("johnny@example.com"); + + UserDto updatedUser = new UserDto(1L, "Johnny", "johnny@example.com"); + + Mockito.when(userService.update(any(UpdateUserDto.class))).thenReturn(updatedUser); + + mockMvc.perform(patch("/users/{id}", 1) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updateUser))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.name").value("Johnny")) + .andExpect(jsonPath("$.email").value("johnny@example.com")); + } + + @Test + @DisplayName("Delete user via DELETE /users/{id}") + void deleteUserTest() throws Exception { + Mockito.doNothing().when(userService).delete(anyLong()); + + mockMvc.perform(delete("/users/{id}", 1)) + .andExpect(status().isNoContent()); + } +} From cbeb9faeea78506cc5a51b7272f42313e0a7c5d6 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Wed, 10 Dec 2025 05:44:37 +0300 Subject: [PATCH 11/23] feat: add exception handler tests --- .../handler/GlobalExceptionHandler.java | 2 +- .../exception/GlobalExceptionHandlerTest.java | 130 ++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 server/src/test/java/ru/practicum/shareit/exception/GlobalExceptionHandlerTest.java diff --git a/server/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java b/server/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java index d0991e1..a73c329 100644 --- a/server/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java +++ b/server/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java @@ -10,7 +10,7 @@ @Slf4j @RestControllerAdvice -class GlobalExceptionHandler { +public class GlobalExceptionHandler { @ExceptionHandler @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) diff --git a/server/src/test/java/ru/practicum/shareit/exception/GlobalExceptionHandlerTest.java b/server/src/test/java/ru/practicum/shareit/exception/GlobalExceptionHandlerTest.java new file mode 100644 index 0000000..e2d053f --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/exception/GlobalExceptionHandlerTest.java @@ -0,0 +1,130 @@ +package ru.practicum.shareit.exception; + + +import org.junit.jupiter.api.DisplayName; +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.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@Import(GlobalExceptionHandlerWebMvcTest.TestController.class) +class GlobalExceptionHandlerWebMvcTest { + + @Autowired + private MockMvc mockMvc; + + @Test + @DisplayName("Handle NotFoundException") + void handleNotFoundException() throws Exception { + mockMvc.perform(get("/test/notfound")) + .andExpect(status().isNotFound()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.name").value("not found")) + .andExpect(jsonPath("$.message").value("entity not found")); + } + + @Test + @DisplayName("Handle ItemUnavailableException") + void handleItemUnavailableException() throws Exception { + mockMvc.perform(get("/test/unavailable")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.name").value("unavailable")) + .andExpect(jsonPath("$.message").value("item unavailable")); + } + + @Test + @DisplayName("Handle DuplicateDataException") + void handleDuplicateDataException() throws Exception { + mockMvc.perform(get("/test/duplicate")) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.name").value("duplicate data")) + .andExpect(jsonPath("$.message").value("duplicate data")); + } + + @Test + @DisplayName("Handle ForbiddenAccessException") + void handleForbiddenAccessException() throws Exception { + mockMvc.perform(get("/test/forbidden")) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.name").value("forbidden")) + .andExpect(jsonPath("$.message").value("forbidden access")); + } + + @Test + @DisplayName("Handle ItemCommentException") + void handleItemCommentException() throws Exception { + mockMvc.perform(get("/test/comment")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.name").value("cant comment")) + .andExpect(jsonPath("$.message").value("cannot comment")); + } + + @Test + @DisplayName("Handle BookingIntersectionException") + void handleBookingIntersectionException() throws Exception { + mockMvc.perform(get("/test/booking")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.name").value("booking intersection")) + .andExpect(jsonPath("$.message").value("booking conflict")); + } + + @Test + @DisplayName("Handle generic Exception") + void handleGenericException() throws Exception { + mockMvc.perform(get("/test/runtime")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.name").value("internal server error")) + .andExpect(jsonPath("$.message").value("An error occurred while processing request")); + } + + @RestController + @RequestMapping("/test") + static class TestController { + + @GetMapping("/notfound") + public void notFound() { + throw new NotFoundException("entity not found"); + } + + @GetMapping("/unavailable") + public void unavailable() { + throw new ItemUnavailableException("item unavailable"); + } + + @GetMapping("/duplicate") + public void duplicate() { + throw new DuplicateDataException("duplicate data"); + } + + @GetMapping("/forbidden") + public void forbidden() { + throw new ForbiddenAccessException("forbidden access"); + } + + @GetMapping("/comment") + public void comment() { + throw new ItemCommentException("cannot comment"); + } + + @GetMapping("/booking") + public void booking() { + throw new BookingIntersectionException("booking conflict"); + } + + @GetMapping("/runtime") + public void runtime() { + throw new RuntimeException("generic error"); + } + } +} From 083e398d2e22efcead2feb3b50084bbe56ebc958 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Wed, 10 Dec 2025 05:51:18 +0300 Subject: [PATCH 12/23] fix: changed class name --- .../practicum/shareit/exception/GlobalExceptionHandlerTest.java | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gateway/src/test/java/ru/practicum/shareit/exception/GlobalExceptionHandlerTest.java b/gateway/src/test/java/ru/practicum/shareit/exception/GlobalExceptionHandlerTest.java index cd7b20f..466eb6b 100644 --- a/gateway/src/test/java/ru/practicum/shareit/exception/GlobalExceptionHandlerTest.java +++ b/gateway/src/test/java/ru/practicum/shareit/exception/GlobalExceptionHandlerTest.java @@ -16,7 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -class GlobalExceptionHandlerUnitTest { +class GlobalExceptionHandlerTest { private final GlobalExceptionHandler handler = new GlobalExceptionHandler(); diff --git a/pom.xml b/pom.xml index b05ff9a..6fd3b24 100644 --- a/pom.xml +++ b/pom.xml @@ -96,7 +96,7 @@ checkstyle.xml true true - true + false From a4d718d84043ac29ccd828002900bdeae52494a0 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Wed, 10 Dec 2025 06:31:39 +0300 Subject: [PATCH 13/23] feat: add bookingcontroller test in gateway --- .../shareit/booking/BookingController.java | 6 + .../handler/GlobalExceptionHandler.java | 8 + .../booking/BookingControllerTest.java | 141 ++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 gateway/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java 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 7584c49..2dadd3c 100644 --- a/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -1,6 +1,7 @@ package ru.practicum.shareit.booking; import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -16,12 +17,17 @@ public class BookingController { private static final String SHARER_USER_ID_HEADER = "X-Sharer-User-Id"; private final RestClient restClient; + @Autowired public BookingController(@Value("${shareit-server.url}") String baseUrl) { this.restClient = RestClient.builder() .baseUrl(baseUrl.concat("/bookings")) .build(); } + public BookingController(RestClient restClient) { + this.restClient = restClient; + } + @PostMapping public ResponseEntity createBooking( @RequestHeader(SHARER_USER_ID_HEADER) long bookerId, diff --git a/gateway/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java b/gateway/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java index 59d5a04..2604b6a 100644 --- a/gateway/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java +++ b/gateway/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java @@ -6,6 +6,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; 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; @@ -63,6 +64,13 @@ public ValidationErrorResponse onConstraintValidationException( return new ValidationErrorResponse(violations); } + @ExceptionHandler(MissingRequestHeaderException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse onMissingRequestHeaderException(MissingRequestHeaderException ex) { + log.error(ex.getMessage()); + return new ErrorResponse("missing header", ex.getMessage()); + } + @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ValidationErrorResponse onMethodArgumentNotValidException( diff --git a/gateway/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java b/gateway/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java new file mode 100644 index 0000000..1f4e4a8 --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java @@ -0,0 +1,141 @@ +package ru.practicum.shareit.booking; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +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.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.booking.dto.BookingDto; + +import java.time.LocalDateTime; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(BookingController.class) +@TestPropertySource(properties = { + "shareit-server.url=http://localhost:9999" +}) +class BookingControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + private static final String HEADER = "X-Sharer-User-Id"; + + @Test + @DisplayName("POST /bookings - should create booking successfully") + void createBooking_ok() throws Exception { + BookingDto dto = new BookingDto( + 1L, + LocalDateTime.now().plusHours(1), + LocalDateTime.now().plusDays(1) + ); + + mockMvc.perform(post("/bookings") + .header(HEADER, 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().is5xxServerError()); + } + + @Test + @DisplayName("POST /bookings - should return 400 when dates are null") + void createBooking_validationError_nullDates() throws Exception { + BookingDto dto = new BookingDto(1L, null, null); + + mockMvc.perform(post("/bookings") + .header(HEADER, 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("POST /bookings - should return 400 when start date is in the past") + void createBooking_validationError_pastStart() throws Exception { + BookingDto dto = new BookingDto( + 1L, + LocalDateTime.now().minusDays(1), + LocalDateTime.now().plusDays(1) + ); + + mockMvc.perform(post("/bookings") + .header(HEADER, 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("POST /bookings - should return 400 when header is missing") + void createBooking_withoutHeader() throws Exception { + BookingDto dto = new BookingDto( + 1L, + LocalDateTime.now().plusHours(1), + LocalDateTime.now().plusDays(1) + ); + + mockMvc.perform(post("/bookings") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("PATCH /bookings/{id} - should approve booking successfully") + void approveBooking_ok() throws Exception { + mockMvc.perform(patch("/bookings/1") + .param("approved", "true") + .header(HEADER, 2L)) + .andExpect(status().is5xxServerError()); + } + + @Test + @DisplayName("GET /bookings/{id} - should return booking successfully") + void getBooking_ok() throws Exception { + mockMvc.perform(get("/bookings/1") + .header(HEADER, 1L)) + .andExpect(status().is5xxServerError()); + } + + @Test + @DisplayName("GET /bookings/{id} - should return 400 when header is missing") + void getBooking_withoutHeader() throws Exception { + mockMvc.perform(get("/bookings/1")) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("GET /bookings - should return all bookings successfully") + void getAllBookings_ok() throws Exception { + mockMvc.perform(get("/bookings") + .param("state", "ALL") + .header(HEADER, 1L)) + .andExpect(status().is5xxServerError()); + } + + @Test + @DisplayName("GET /bookings - should return 400 for invalid state") + void getAllBookings_invalidState() throws Exception { + mockMvc.perform(get("/bookings") + .param("state", "INVALID") + .header(HEADER, 1L)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("GET /bookings/owner - should return bookings by owner successfully") + void getBookingsByOwner_ok() throws Exception { + mockMvc.perform(get("/bookings/owner") + .param("state", "WAITING") + .header(HEADER, 1L)) + .andExpect(status().is5xxServerError()); + } +} From 1029742a6c5ceebf156d0768168a29742cafda76 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Wed, 10 Dec 2025 06:47:04 +0300 Subject: [PATCH 14/23] feat: more controller tests for gateway --- .../shareit/item/ItemController.java | 2 +- .../shareit/item/ItemControllerTest.java | 151 ++++++++++++++++++ .../shareit/user/UserControllerTest.java | 93 +++++++++++ .../shareit/item/ItemController.java | 4 +- 4 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 gateway/src/test/java/ru/practicum/shareit/item/ItemControllerTest.java create mode 100644 gateway/src/test/java/ru/practicum/shareit/user/UserControllerTest.java diff --git a/gateway/src/main/java/ru/practicum/shareit/item/ItemController.java b/gateway/src/main/java/ru/practicum/shareit/item/ItemController.java index 0b5be5d..9e399c3 100644 --- a/gateway/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/gateway/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -78,7 +78,7 @@ public ResponseEntity updateItem( @GetMapping("/search") public ResponseEntity searchItems( - @RequestParam(name = "text") String query + @RequestParam(name = "text", required = false) String query ) { return restClient.get() .uri(uriBuilder -> uriBuilder diff --git a/gateway/src/test/java/ru/practicum/shareit/item/ItemControllerTest.java b/gateway/src/test/java/ru/practicum/shareit/item/ItemControllerTest.java new file mode 100644 index 0000000..c837d8a --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/item/ItemControllerTest.java @@ -0,0 +1,151 @@ +package ru.practicum.shareit.item; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +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.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.item.dto.NewCommentDto; +import ru.practicum.shareit.item.dto.NewItemDto; +import ru.practicum.shareit.item.dto.UpdateItemDto; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(ItemController.class) +@TestPropertySource(properties = { + "shareit-server.url=http://localhost:9999" +}) +class ItemControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + private static final String HEADER = "X-Sharer-User-Id"; + + @Test + @DisplayName("GET /items/{id} - should return item successfully") + void getItem_ok() throws Exception { + mockMvc.perform(get("/items/1") + .header(HEADER, 1L)) + .andExpect(status().is5xxServerError()); + } + + @Test + @DisplayName("GET /items/{id} - should return 400 when header is missing") + void getItem_withoutHeader() throws Exception { + mockMvc.perform(get("/items/1")) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("GET /items - should return all items successfully") + void getItems_ok() throws Exception { + mockMvc.perform(get("/items") + .header(HEADER, 1L)) + .andExpect(status().is5xxServerError()); + } + + @Test + @DisplayName("GET /items - should return 400 when header is missing") + void getItems_withoutHeader() throws Exception { + mockMvc.perform(get("/items")) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("POST /items - should return 400 when name is blank") + void createItem_blankName() throws Exception { + NewItemDto dto = new NewItemDto("", "Valid description", true, null); + + mockMvc.perform(post("/items") + .header(HEADER, 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("POST /items - should return 400 when description is blank") + void createItem_blankDescription() throws Exception { + NewItemDto dto = new NewItemDto("Valid name", "", true, null); + + mockMvc.perform(post("/items") + .header(HEADER, 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("POST /items - should return 400 when available is null") + void createItem_nullAvailable() throws Exception { + NewItemDto dto = new NewItemDto("Valid name", "Valid description", null, null); + + mockMvc.perform(post("/items") + .header(HEADER, 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("PATCH /items/{id} - should update item successfully") + void updateItem_ok() throws Exception { + UpdateItemDto dto = new UpdateItemDto("Updated name", "Updated description", true); + + mockMvc.perform(patch("/items/1") + .header(HEADER, 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().is5xxServerError()); + } + + @Test + @DisplayName("PATCH /items/{id} - should return 400 when header is missing") + void updateItem_withoutHeader() throws Exception { + UpdateItemDto dto = new UpdateItemDto("Updated name", "Updated description", true); + + mockMvc.perform(patch("/items/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("GET /items/search - should return search results successfully") + void searchItems_ok() throws Exception { + mockMvc.perform(get("/items/search") + .param("text", "query")) + .andExpect(status().is5xxServerError()); + } + + @Test + @DisplayName("POST /items/{id}/comment - should create comment successfully") + void createComment_ok() throws Exception { + NewCommentDto dto = new NewCommentDto("Nice item!"); + + mockMvc.perform(post("/items/1/comment") + .header(HEADER, 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().is5xxServerError()); + } + + @Test + @DisplayName("POST /items/{id}/comment - should return 400 when header is missing") + void createComment_withoutHeader() throws Exception { + NewCommentDto dto = new NewCommentDto("Nice item!"); + + mockMvc.perform(post("/items/1/comment") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isBadRequest()); + } +} diff --git a/gateway/src/test/java/ru/practicum/shareit/user/UserControllerTest.java b/gateway/src/test/java/ru/practicum/shareit/user/UserControllerTest.java new file mode 100644 index 0000000..565bc0b --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/user/UserControllerTest.java @@ -0,0 +1,93 @@ +package ru.practicum.shareit.user; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +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.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.user.dto.NewUserDto; +import ru.practicum.shareit.user.dto.UpdateUserDto; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(UserController.class) +@TestPropertySource(properties = { + "shareit-server.url=http://localhost:9999" +}) +class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Test + @DisplayName("GET /users - should return all users successfully") + void getAllUsers_ok() throws Exception { + mockMvc.perform(get("/users")) + .andExpect(status().is5xxServerError()); + } + + @Test + @DisplayName("GET /users/{id} - should return user successfully") + void getUser_ok() throws Exception { + mockMvc.perform(get("/users/1")) + .andExpect(status().is5xxServerError()); + } + + @Test + @DisplayName("POST /users - should create user successfully") + void createUser_ok() throws Exception { + NewUserDto dto = new NewUserDto("John Doe", "john@example.com"); + + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().is5xxServerError()); + } + + @Test + @DisplayName("POST /users - should return 400 when name is blank") + void createUser_blankName() throws Exception { + NewUserDto dto = new NewUserDto("", "john@example.com"); + + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("PATCH /users/{id} - should update user successfully") + void updateUser_ok() throws Exception { + UpdateUserDto dto = new UpdateUserDto(1L, "John Doe","email@email" ); + + mockMvc.perform(patch("/users/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().is5xxServerError()); + } + + @Test + @DisplayName("PATCH /users/{id} - should return 400 when update data is invalid") + void updateUser_invalidData() throws Exception { + UpdateUserDto dto = new UpdateUserDto(1L, "John Doe", "not-an-email"); + + mockMvc.perform(patch("/users/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("DELETE /users/{id} - should delete user successfully") + void deleteUser_ok() throws Exception { + mockMvc.perform(delete("/users/1")) + .andExpect(status().is2xxSuccessful()); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/item/ItemController.java b/server/src/main/java/ru/practicum/shareit/item/ItemController.java index bc3cb96..aca8ed7 100644 --- a/server/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/server/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -51,9 +51,9 @@ public ItemDto updateItem( @GetMapping("/search") public Collection searchItems( - @RequestParam(name = "text") String query + @RequestParam(name = "text", required = false) String query ) { - return itemService.searchItems(query); + return itemService.searchItems(query == null ? "" : query); } @PostMapping("/{itemId}/comment") From 68e107d0fc45292d047bb86a6c637a8d0813d254 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Wed, 10 Dec 2025 07:00:08 +0300 Subject: [PATCH 15/23] feat: more gateway tests --- .../exception/GlobalExceptionHandlerTest.java | 28 ++++++ .../shareit/item/ItemControllerTest.java | 24 +++++ .../request/ItemRequestControllerTest.java | 93 +++++++++++++++++++ .../dto/NewItemRequestDtoJsonTest.java | 3 +- 4 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 gateway/src/test/java/ru/practicum/shareit/request/ItemRequestControllerTest.java rename gateway/src/test/java/ru/practicum/shareit/{item => request}/dto/NewItemRequestDtoJsonTest.java (90%) diff --git a/gateway/src/test/java/ru/practicum/shareit/exception/GlobalExceptionHandlerTest.java b/gateway/src/test/java/ru/practicum/shareit/exception/GlobalExceptionHandlerTest.java index 466eb6b..7480c3c 100644 --- a/gateway/src/test/java/ru/practicum/shareit/exception/GlobalExceptionHandlerTest.java +++ b/gateway/src/test/java/ru/practicum/shareit/exception/GlobalExceptionHandlerTest.java @@ -7,6 +7,10 @@ import org.hibernate.validator.internal.engine.path.PathImpl; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; import ru.practicum.shareit.exception.dto.ErrorResponse; import ru.practicum.shareit.exception.dto.ValidationErrorResponse; import ru.practicum.shareit.exception.dto.Violation; @@ -14,6 +18,7 @@ import java.util.Set; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; class GlobalExceptionHandlerTest { @@ -108,4 +113,27 @@ void handleGenericException() { assertEquals("internal server error", response.name()); assertEquals("An error occurred while processing request", response.message()); } + + @Test + @DisplayName("Handle HttpServerErrorException") + void testHttpServerErrorExceptionHandler() { + HttpServerErrorException ex = + HttpServerErrorException.create(HttpStatusCode.valueOf(500), "Internal Server Error", null, "ErrorBody".getBytes(), null); + + ResponseEntity response = handler.onHttpServerErrorException(ex); + + assertEquals(500, response.getStatusCodeValue()); + assertArrayEquals("ErrorBody".getBytes(), (byte[]) response.getBody()); + } + + @Test + @DisplayName("Handle HttpClientErrorException") + void testHttpClientErrorExceptionHandler() { + HttpClientErrorException ex = HttpClientErrorException.create(HttpStatusCode.valueOf(400), "Bad Request", null, "ClientError".getBytes(), null); + + ResponseEntity response = handler.onHttpClientErrorException(ex); + + assertEquals(400, response.getStatusCodeValue()); + assertArrayEquals("ClientError".getBytes(), (byte[]) response.getBody()); + } } diff --git a/gateway/src/test/java/ru/practicum/shareit/item/ItemControllerTest.java b/gateway/src/test/java/ru/practicum/shareit/item/ItemControllerTest.java index c837d8a..1fe4b3b 100644 --- a/gateway/src/test/java/ru/practicum/shareit/item/ItemControllerTest.java +++ b/gateway/src/test/java/ru/practicum/shareit/item/ItemControllerTest.java @@ -59,6 +59,30 @@ void getItems_withoutHeader() throws Exception { .andExpect(status().isBadRequest()); } + + @Test + @DisplayName("POST /items - should create item successfully") + void createItem_ok() throws Exception { + NewItemDto dto = new NewItemDto("Drill", "Powerful drill", true, null); + + mockMvc.perform(post("/items") + .header(HEADER, 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().is5xxServerError()); + } + + + @Test + @DisplayName("POST /items - should return 400 when header is missing") + void createItem_withoutHeader() throws Exception { + NewItemDto dto = new NewItemDto("Drill", "Powerful drill", true, null); + + mockMvc.perform(post("/items") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isBadRequest()); + } @Test @DisplayName("POST /items - should return 400 when name is blank") void createItem_blankName() throws Exception { diff --git a/gateway/src/test/java/ru/practicum/shareit/request/ItemRequestControllerTest.java b/gateway/src/test/java/ru/practicum/shareit/request/ItemRequestControllerTest.java new file mode 100644 index 0000000..e5cbd77 --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/request/ItemRequestControllerTest.java @@ -0,0 +1,93 @@ +package ru.practicum.shareit.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +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.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.request.dto.NewItemRequestDto; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(ItemRequestController.class) +@TestPropertySource(properties = { + "shareit-server.url=http://localhost:9999" +}) +class ItemRequestControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + private static final String HEADER = "X-Sharer-User-Id"; + + @Test + @DisplayName("POST /requests - should create request successfully") + void createRequest_ok() throws Exception { + NewItemRequestDto dto = new NewItemRequestDto("Need a drill"); + + mockMvc.perform(post("/requests") + .header(HEADER, 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().is5xxServerError()); + } + + @Test + @DisplayName("POST /requests - should return 400 when description is blank") + void createRequest_blankDescription() throws Exception { + NewItemRequestDto dto = new NewItemRequestDto(""); + + mockMvc.perform(post("/requests") + .header(HEADER, 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("POST /requests - should return 400 when header is missing") + void createRequest_withoutHeader() throws Exception { + NewItemRequestDto dto = new NewItemRequestDto("Need a drill"); + + mockMvc.perform(post("/requests") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("GET /requests - should return all requests of user successfully") + void getAllRequestsOfUser_ok() throws Exception { + mockMvc.perform(get("/requests") + .header(HEADER, 1L)) + .andExpect(status().is5xxServerError()); + } + + @Test + @DisplayName("GET /requests - should return 400 when header is missing") + void getAllRequestsOfUser_withoutHeader() throws Exception { + mockMvc.perform(get("/requests")) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("GET /requests/all - should return all requests successfully") + void getAllRequests_ok() throws Exception { + mockMvc.perform(get("/requests/all")) + .andExpect(status().is5xxServerError()); + } + + @Test + @DisplayName("GET /requests/{id} - should return request by id successfully") + void getRequestById_ok() throws Exception { + mockMvc.perform(get("/requests/1")) + .andExpect(status().is5xxServerError()); + } +} diff --git a/gateway/src/test/java/ru/practicum/shareit/item/dto/NewItemRequestDtoJsonTest.java b/gateway/src/test/java/ru/practicum/shareit/request/dto/NewItemRequestDtoJsonTest.java similarity index 90% rename from gateway/src/test/java/ru/practicum/shareit/item/dto/NewItemRequestDtoJsonTest.java rename to gateway/src/test/java/ru/practicum/shareit/request/dto/NewItemRequestDtoJsonTest.java index 0367f7a..c13bcdb 100644 --- a/gateway/src/test/java/ru/practicum/shareit/item/dto/NewItemRequestDtoJsonTest.java +++ b/gateway/src/test/java/ru/practicum/shareit/request/dto/NewItemRequestDtoJsonTest.java @@ -1,4 +1,4 @@ -package ru.practicum.shareit.item.dto; +package ru.practicum.shareit.request.dto; import jakarta.validation.Validation; import jakarta.validation.Validator; @@ -6,7 +6,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.json.JsonTest; import org.springframework.boot.test.json.JacksonTester; -import ru.practicum.shareit.request.dto.NewItemRequestDto; import static org.assertj.core.api.Assertions.assertThat; From 91aeb9e41614a5226cab16c21aad54e5524d9e40 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Wed, 10 Dec 2025 07:05:09 +0300 Subject: [PATCH 16/23] feat: enum tests --- .../shareit/booking/BookingStateTest.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 gateway/src/test/java/ru/practicum/shareit/booking/BookingStateTest.java diff --git a/gateway/src/test/java/ru/practicum/shareit/booking/BookingStateTest.java b/gateway/src/test/java/ru/practicum/shareit/booking/BookingStateTest.java new file mode 100644 index 0000000..8439b62 --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/booking/BookingStateTest.java @@ -0,0 +1,72 @@ +package ru.practicum.shareit.booking; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +class BookingStateTest { + + @Test + @DisplayName("fromString - should return ALL for 'ALL'") + void fromString_all() { + Optional state = BookingState.fromString("ALL"); + assertTrue(state.isPresent()); + assertEquals(BookingState.ALL, state.get()); + } + + @Test + @DisplayName("fromString - should return CURRENT for 'CURRENT' (case insensitive)") + void fromString_current_caseInsensitive() { + Optional state = BookingState.fromString("current"); + assertTrue(state.isPresent()); + assertEquals(BookingState.CURRENT, state.get()); + } + + @Test + @DisplayName("fromString - should return PAST for 'PAST'") + void fromString_past() { + Optional state = BookingState.fromString("PAST"); + assertTrue(state.isPresent()); + assertEquals(BookingState.PAST, state.get()); + } + + @Test + @DisplayName("fromString - should return FUTURE for 'FUTURE'") + void fromString_future() { + Optional state = BookingState.fromString("FUTURE"); + assertTrue(state.isPresent()); + assertEquals(BookingState.FUTURE, state.get()); + } + + @Test + @DisplayName("fromString - should return WAITING for 'WAITING'") + void fromString_waiting() { + Optional state = BookingState.fromString("WAITING"); + assertTrue(state.isPresent()); + assertEquals(BookingState.WAITING, state.get()); + } + + @Test + @DisplayName("fromString - should return REJECTED for 'REJECTED'") + void fromString_rejected() { + Optional state = BookingState.fromString("REJECTED"); + assertTrue(state.isPresent()); + assertEquals(BookingState.REJECTED, state.get()); + } + + @Test + @DisplayName("fromString - should return empty for invalid string") + void fromString_invalid() { + Optional state = BookingState.fromString("INVALID_STATE"); + assertTrue(state.isEmpty()); + } + + @Test + @DisplayName("fromString - should handle null input") + void fromString_null() { + assertThrows(NullPointerException.class, () -> BookingState.fromString(null)); + } +} From 14351618cf27b9d14ea44def961fbdcc58cd8c51 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Wed, 10 Dec 2025 07:08:42 +0300 Subject: [PATCH 17/23] fix: remove unused constructor --- .../java/ru/practicum/shareit/booking/BookingController.java | 4 ---- 1 file changed, 4 deletions(-) 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 2dadd3c..e906bf0 100644 --- a/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -24,10 +24,6 @@ public BookingController(@Value("${shareit-server.url}") String baseUrl) { .build(); } - public BookingController(RestClient restClient) { - this.restClient = restClient; - } - @PostMapping public ResponseEntity createBooking( @RequestHeader(SHARER_USER_ID_HEADER) long bookerId, From ca4659378ec76aa1496c970567f532a514cf528e Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Wed, 10 Dec 2025 07:10:56 +0300 Subject: [PATCH 18/23] fix --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6fd3b24..a0c4814 100644 --- a/pom.xml +++ b/pom.xml @@ -162,7 +162,7 @@ LINE COVEREDRATIO - 0.9 + 0.7 BRANCH From 307d9b831710254527c724c553a840a59eabea1e Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Wed, 10 Dec 2025 07:30:47 +0300 Subject: [PATCH 19/23] feat: add service and mapper tests for server --- .../shareit/booking/BookingState.java | 14 -- .../ru/practicum/shareit/ShareItTests.java | 2 +- .../shareit/booking/BookingMapperTest.java | 101 ++++++++++++ .../shareit/booking/BookingServiceTest.java | 148 ++++++++++++++++++ .../shareit/item/CommentMapperTest.java | 50 ++++++ .../shareit/item/ItemMapperTest.java | 117 ++++++++++++++ .../shareit/item/ItemServiceTest.java | 145 +++++++++++++++++ .../request/ItemRequestMapperTest.java | 70 +++++++++ .../request/ItemRequestServiceTest.java | 126 +++++++++++++++ .../shareit/user/UserMapperTest.java | 61 ++++++++ .../shareit/user/UserServiceTest.java | 128 +++++++++++++++ 11 files changed, 947 insertions(+), 15 deletions(-) create mode 100644 server/src/test/java/ru/practicum/shareit/booking/BookingMapperTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/booking/BookingServiceTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/item/CommentMapperTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/item/ItemMapperTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/item/ItemServiceTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/request/ItemRequestMapperTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/user/UserMapperTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/user/UserServiceTest.java diff --git a/server/src/main/java/ru/practicum/shareit/booking/BookingState.java b/server/src/main/java/ru/practicum/shareit/booking/BookingState.java index b59f4ff..3d41a9a 100644 --- a/server/src/main/java/ru/practicum/shareit/booking/BookingState.java +++ b/server/src/main/java/ru/practicum/shareit/booking/BookingState.java @@ -1,7 +1,5 @@ package ru.practicum.shareit.booking; -import java.util.Optional; - public enum BookingState { ALL, CURRENT, @@ -10,16 +8,4 @@ public enum BookingState { WAITING, REJECTED; - public static Optional fromString(String state) { - BookingState bookingState = switch (state.toUpperCase()) { - case "ALL" -> ALL; - case "CURRENT" -> CURRENT; - case "PAST" -> PAST; - case "FUTURE" -> FUTURE; - case "WAITING" -> WAITING; - case "REJECTED" -> REJECTED; - default -> null; - }; - return Optional.ofNullable(bookingState); - } } diff --git a/server/src/test/java/ru/practicum/shareit/ShareItTests.java b/server/src/test/java/ru/practicum/shareit/ShareItTests.java index 06d43e9..bea30d4 100644 --- a/server/src/test/java/ru/practicum/shareit/ShareItTests.java +++ b/server/src/test/java/ru/practicum/shareit/ShareItTests.java @@ -6,7 +6,7 @@ @SpringBootTest() @TestPropertySource(locations = "classpath:application-test.properties") -class ShareItTests { +class ShareItServerTest { @Test void contextLoads() { diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingMapperTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingMapperTest.java new file mode 100644 index 0000000..e4c61af --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingMapperTest.java @@ -0,0 +1,101 @@ +package ru.practicum.shareit.booking; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingInfoDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +class BookingMapperTest { + + @Test + @DisplayName("toBookingResponseDto - should map Booking to BookingResponseDto correctly") + void toBookingResponseDto_ok() { + User user = new User(1L, "John", "john@example.com"); + Item item = new Item(2L, "Drill", "Powerful drill", true, user, null); + Booking booking = new Booking(3L, + LocalDateTime.now(), + LocalDateTime.now().plusDays(1), + item, + user, + BookingStatus.APPROVED); + + BookingResponseDto dto = BookingMapper.toBookingResponseDto(booking); + + assertEquals(booking.getId(), dto.id()); + assertEquals(booking.getStartTime(), dto.start()); + assertEquals(booking.getEndTime(), dto.end()); + + ItemDto itemDto = dto.item(); + assertEquals(item.getId(), itemDto.id()); + assertEquals(item.getName(), itemDto.name()); + assertEquals(item.getDescription(), itemDto.description()); + assertEquals(item.isAvailable(), itemDto.available()); + + UserDto userDto = dto.booker(); + assertEquals(user.getId(), userDto.id()); + assertEquals(user.getName(), userDto.name()); + assertEquals(user.getEmail(), userDto.email()); + + assertEquals(booking.getStatus(), dto.status()); + } + + @Test + @DisplayName("fromDto - should create Booking from BookingDto") + void fromDto_ok() { + User user = new User(1L, "John", "john@example.com"); + Item item = new Item(2L, "Drill", "Powerful drill", true, user, null); + BookingDto bookingDto = new BookingDto(2L, + LocalDateTime.now(), + LocalDateTime.now().plusDays(1)); + + Booking booking = BookingMapper.fromDto(bookingDto, user, item); + + assertNull(booking.getId()); + assertEquals(bookingDto.start(), booking.getStartTime()); + assertEquals(bookingDto.end(), booking.getEndTime()); + assertEquals(user, booking.getBooker()); + assertEquals(item, booking.getItem()); + assertEquals(BookingStatus.WAITING, booking.getStatus()); + } + + @Test + @DisplayName("toBookingInfoDto - should map Booking to BookingInfoDto correctly") + void toBookingInfoDto_ok() { + User user = new User(1L, "John", "john@example.com"); + Item item = new Item(2L, "Drill", "Powerful drill", true, user, null); + Booking booking = new Booking(3L, + LocalDateTime.now(), + LocalDateTime.now().plusDays(1), + item, + user, + BookingStatus.APPROVED); + + Optional optDto = BookingMapper.toBookingInfoDto(booking); + + assertTrue(optDto.isPresent()); + BookingInfoDto dto = optDto.get(); + assertEquals(booking.getId(), dto.id()); + assertEquals(user.getId(), dto.bookerId()); + assertEquals(booking.getStartTime(), dto.start()); + assertEquals(booking.getEndTime(), dto.end()); + } + + @Test + @DisplayName("toBookingInfoDto - should return empty for null input") + void toBookingInfoDto_null() { + Optional optDto = BookingMapper.toBookingInfoDto(null); + assertTrue(optDto.isEmpty()); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingServiceTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingServiceTest.java new file mode 100644 index 0000000..8d21034 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingServiceTest.java @@ -0,0 +1,148 @@ +package ru.practicum.shareit.booking; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.*; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; +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.exception.*; +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 java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class BookingServiceTest { + + @Mock + private BookingRepository bookingRepository; + @Mock + private ItemRepository itemRepository; + @Mock + private UserRepository userRepository; + + @InjectMocks + private BookingServiceImpl bookingService; + + private User booker; + private User owner; + private Item item; + private BookingDto bookingDto; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + + booker = new User(1L, "Booker", "booker@example.com"); + owner = new User(2L, "Owner", "owner@example.com"); + item = new Item(1L, "Drill", "Powerful drill", true, owner, null); + + bookingDto = new BookingDto( + item.getId(), + LocalDateTime.now().plusHours(1), + LocalDateTime.now().plusHours(2) + ); + } + + @Test + @DisplayName("createBooking - should create booking successfully") + void createBooking_ok() { + when(userRepository.findById(booker.getId())).thenReturn(Optional.of(booker)); + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + when(bookingRepository.findApprovedIntersectingBookings(anyLong(), any(), any())) + .thenReturn(List.of()); + when(bookingRepository.save(any(Booking.class))).thenAnswer(invocation -> { + Booking b = invocation.getArgument(0); + b.setId(10L); + return b; + }); + + BookingResponseDto response = bookingService.createBooking(booker.getId(), bookingDto); + + assertNotNull(response); + assertEquals(10L, response.id()); + verify(bookingRepository).save(any(Booking.class)); + } + + @Test + @DisplayName("createBooking - should throw ItemUnavailableException when item is unavailable") + void createBooking_itemUnavailable() { + item.setAvailable(false); + when(userRepository.findById(booker.getId())).thenReturn(Optional.of(booker)); + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + + assertThrows(ItemUnavailableException.class, () -> + bookingService.createBooking(booker.getId(), bookingDto)); + } + + @Test + @DisplayName("createBooking - should throw BookingIntersectionException when dates intersect") + void createBooking_dateIntersection() { + when(userRepository.findById(booker.getId())).thenReturn(Optional.of(booker)); + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + when(bookingRepository.findApprovedIntersectingBookings(anyLong(), any(), any())) + .thenReturn(List.of(new Booking())); + + assertThrows(BookingIntersectionException.class, () -> + bookingService.createBooking(booker.getId(), bookingDto)); + } + + @Test + @DisplayName("approveBooking - should approve booking successfully") + void approveBooking_ok() { + Booking booking = new Booking(1L, bookingDto.start(), bookingDto.end(), item, booker, BookingStatus.WAITING); + when(bookingRepository.findById(booking.getId())).thenReturn(Optional.of(booking)); + when(userRepository.findById(owner.getId())).thenReturn(Optional.of(owner)); + when(bookingRepository.save(any())).thenReturn(booking); + + BookingResponseDto response = bookingService.approveBooking(booking.getId(), true, owner.getId()); + + assertEquals(BookingStatus.APPROVED, booking.getStatus()); + assertNotNull(response); + } + + @Test + @DisplayName("approveBooking - should throw ForbiddenAccessException when non-owner approves") + void approveBooking_notOwner() { + Booking booking = new Booking(1L, bookingDto.start(), bookingDto.end(), item, booker, BookingStatus.WAITING); + when(bookingRepository.findById(booking.getId())).thenReturn(Optional.of(booking)); + + assertThrows(ForbiddenAccessException.class, () -> + bookingService.approveBooking(booking.getId(), true, booker.getId())); + } + + @Test + @DisplayName("getBookingById - should return booking for booker") + void getBookingById_ok() { + Booking booking = new Booking(1L, bookingDto.start(), bookingDto.end(), item, booker, BookingStatus.WAITING); + when(bookingRepository.findById(booking.getId())).thenReturn(Optional.of(booking)); + when(userRepository.findById(booker.getId())).thenReturn(Optional.of(booker)); + + BookingResponseDto response = bookingService.getBookingById(booking.getId(), booker.getId()); + + assertNotNull(response); + assertEquals(booking.getId(), response.id()); + } + + @Test + @DisplayName("getBookingById - should throw ForbiddenAccessException when user is neither owner nor booker") + void getBookingById_noAccess() { + User other = new User(3L, "Other", "other@example.com"); + Booking booking = new Booking(1L, bookingDto.start(), bookingDto.end(), item, booker, BookingStatus.WAITING); + when(bookingRepository.findById(booking.getId())).thenReturn(Optional.of(booking)); + when(userRepository.findById(other.getId())).thenReturn(Optional.of(other)); + + assertThrows(ForbiddenAccessException.class, () -> + bookingService.getBookingById(booking.getId(), other.getId())); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/item/CommentMapperTest.java b/server/src/test/java/ru/practicum/shareit/item/CommentMapperTest.java new file mode 100644 index 0000000..2e967c2 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/CommentMapperTest.java @@ -0,0 +1,50 @@ +package ru.practicum.shareit.item; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.dto.NewCommentDto; +import ru.practicum.shareit.item.mapper.CommentMapper; +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; + +import static org.junit.jupiter.api.Assertions.*; + +class CommentMapperTest { + + @Test + @DisplayName("toDto - should map Comment to CommentDto correctly") + void toDto_ok() { + User author = new User(1L, "John", "john@example.com"); + Item item = new Item(2L, "Drill", "Powerful drill", true, author, null); + LocalDateTime created = LocalDateTime.now(); + Comment comment = new Comment(3L, "Nice item", item, author, created); + + CommentDto dto = CommentMapper.toDto(comment); + + assertEquals(comment.getId(), dto.id()); + assertEquals(comment.getText(), dto.text()); + assertEquals(comment.getAuthor().getName(), dto.authorName()); + assertEquals(comment.getCreatedAt(), dto.created()); + } + + @Test + @DisplayName("toEntity - should map NewCommentDto to Comment correctly") + void toEntity_ok() { + User author = new User(1L, "John", "john@example.com"); + Item item = new Item(2L, "Drill", "Powerful drill", true, author, null); + NewCommentDto newCommentDto = new NewCommentDto("Great item!"); + LocalDateTime now = LocalDateTime.now(); + + Comment comment = CommentMapper.toEntity(newCommentDto, author, item, now); + + assertNull(comment.getId()); + assertEquals(newCommentDto.text(), comment.getText()); + assertEquals(author, comment.getAuthor()); + assertEquals(item, comment.getItem()); + assertEquals(now, comment.getCreatedAt()); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemMapperTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemMapperTest.java new file mode 100644 index 0000000..7db30f6 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/ItemMapperTest.java @@ -0,0 +1,117 @@ +package ru.practicum.shareit.item; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.item.dto.*; +import ru.practicum.shareit.item.mapper.ItemMapper; +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; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class ItemMapperTest { + + @Test + @DisplayName("toItemDto - should map Item to ItemDto correctly") + void toItemDto_ok() { + User owner = new User(1L, "Owner", "owner@example.com"); + Item item = new Item(2L, "Drill", "Powerful drill", true, owner, null); + + ItemDto dto = ItemMapper.toItemDto(item); + + assertEquals(item.getId(), dto.id()); + assertEquals(item.getName(), dto.name()); + assertEquals(item.getDescription(), dto.description()); + assertEquals(item.isAvailable(), dto.available()); + } + + @Test + @DisplayName("toItem - should create Item from NewItemDto") + void toItem_ok() { + NewItemDto newItemDto = new NewItemDto("Drill", "Powerful drill", true, null); + + Item item = ItemMapper.toItem(newItemDto); + + assertEquals(newItemDto.name(), item.getName()); + assertEquals(newItemDto.description(), item.getDescription()); + assertEquals(newItemDto.available(), item.isAvailable()); + } + + @Test + @DisplayName("updateItem - should update fields of Item if present") + void updateItem_ok() { + Item item = new Item(); + item.setName("Old Name"); + item.setDescription("Old Desc"); + item.setAvailable(false); + + UpdateItemDto update = new UpdateItemDto("New Name", "New Desc", true); + + Item updated = ItemMapper.updateItem(item, update); + + assertEquals("New Name", updated.getName()); + assertEquals("New Desc", updated.getDescription()); + assertTrue(updated.isAvailable()); + } + + @Test + @DisplayName("updateItem - should not update fields if null in UpdateItemDto") + void updateItem_partial() { + Item item = new Item(); + item.setName("Old Name"); + item.setDescription("Old Desc"); + item.setAvailable(false); + + UpdateItemDto update = new UpdateItemDto(null, null, null); + + Item updated = ItemMapper.updateItem(item, update); + + assertEquals("Old Name", updated.getName()); + assertEquals("Old Desc", updated.getDescription()); + assertFalse(updated.isAvailable()); + } + + @Test + @DisplayName("toItemWithBookingDatesDto - should map Item with bookings and comments correctly") + void toItemWithBookingDatesDto_ok() { + User owner = new User(1L, "Owner", "owner@example.com"); + Item item = new Item(2L, "Drill", "Powerful drill", true, owner, null); + + User booker = new User(3L, "John", "john@example.com"); + Booking lastBooking = new Booking(10L, LocalDateTime.now().minusDays(2), + LocalDateTime.now().minusDays(1), item, booker, null); + Booking nextBooking = new Booking(11L, LocalDateTime.now().plusDays(1), + LocalDateTime.now().plusDays(2), item, booker, null); + + Comment comment = new Comment(1L, "Nice item", item, booker, LocalDateTime.now()); + + ItemWithBookingDto dto = ItemMapper.toItemWithBookingDatesDto(item, nextBooking, lastBooking, List.of(comment)); + + assertEquals(item.getId(), dto.id()); + assertEquals(item.getName(), dto.name()); + assertEquals(item.getDescription(), dto.description()); + assertEquals(item.isAvailable(), dto.available()); + assertNotNull(dto.lastBooking()); + assertNotNull(dto.nextBooking()); + assertNotNull(dto.comments()); + assertEquals(1, dto.comments().size()); + } + + @Test + @DisplayName("toItemForRequestDto - should map Item to ItemForRequestDto correctly") + void toItemForRequestDto_ok() { + User owner = new User(1L, "Owner", "owner@example.com"); + Item item = new Item(2L, "Drill", "Powerful drill", true, owner, null); + + ItemForRequestDto dto = ItemMapper.toItemForRequestDto(item); + + assertEquals(item.getId(), dto.id()); + assertEquals(item.getName(), dto.name()); + assertEquals(owner.getId(), dto.ownerId()); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemServiceTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemServiceTest.java new file mode 100644 index 0000000..94acd9d --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/ItemServiceTest.java @@ -0,0 +1,145 @@ +package ru.practicum.shareit.item; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +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.exception.ForbiddenAccessException; +import ru.practicum.shareit.exception.ItemCommentException; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.item.dto.*; +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.request.repository.ItemRequestRepository; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class ItemServiceTest { + + @Mock + private ItemRepository itemRepository; + @Mock + private UserRepository userRepository; + @Mock + private BookingRepository bookingRepository; + @Mock + private CommentRepository commentRepository; + @Mock + private ItemRequestRepository requestRepository; + + @InjectMocks + private ItemServiceImpl itemService; + + private User owner; + private Item item; + private NewItemDto newItemDto; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + owner = new User(1L, "Owner", "owner@example.com"); + item = new Item(1L, "Drill", "Powerful drill", true, owner, null); + newItemDto = new NewItemDto("Hammer", "Heavy hammer", true, null); + } + + @Test + @DisplayName("saveItem - should save item successfully without request") + void saveItem_ok() { + when(userRepository.findById(owner.getId())).thenReturn(Optional.of(owner)); + when(itemRepository.save(any())).thenAnswer(invocation -> { + Item i = invocation.getArgument(0); + i.setId(10L); + return i; + }); + + ItemDto saved = itemService.saveItem(owner.getId(), newItemDto); + + assertNotNull(saved); + assertEquals(10L, saved.id()); + assertEquals("Hammer", saved.name()); + verify(itemRepository).save(any()); + } + + @Test + @DisplayName("saveItem - should throw NotFoundException when user not found") + void saveItem_userNotFound() { + when(userRepository.findById(owner.getId())).thenReturn(Optional.empty()); + assertThrows(NotFoundException.class, () -> itemService.saveItem(owner.getId(), newItemDto)); + } + + @Test + @DisplayName("updateItem - should update item successfully") + void updateItem_ok() { + UpdateItemDto updateDto = new UpdateItemDto("Updated Drill", null, null); + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + when(itemRepository.save(any())).thenReturn(item); + + ItemDto updated = itemService.updateItem(owner.getId(), item.getId(), updateDto); + + assertEquals("Updated Drill", updated.name()); + verify(itemRepository).save(any()); + } + + @Test + @DisplayName("updateItem - should throw ForbiddenAccessException for non-owner") + void updateItem_notOwner() { + UpdateItemDto updateDto = new UpdateItemDto("Updated Drill", null, null); + User other = new User(2L, "Other", "other@example.com"); + item.setOwner(owner); + + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + + assertThrows(ForbiddenAccessException.class, () -> itemService.updateItem(other.getId(), item.getId(), updateDto)); + } + + @Test + @DisplayName("createComment - should create comment successfully") + void createComment_ok() { + User author = new User(2L, "Author", "author@example.com"); + NewCommentDto newComment = new NewCommentDto("Great item!"); + Booking booking = new Booking(1L, LocalDateTime.now().minusDays(2), LocalDateTime.now().minusDays(1), item, author, BookingStatus.APPROVED); + Comment savedComment = new Comment(1L, "Great item!", item, author, LocalDateTime.now()); + + when(userRepository.findById(author.getId())).thenReturn(Optional.of(author)); + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + when(bookingRepository.findByBookerIdAndItemIdAndStatusAndEndTimeIsBefore( + anyLong(), anyLong(), any(), any() + )).thenReturn(Optional.of(booking)); + when(commentRepository.save(any())).thenReturn(savedComment); + + CommentDto dto = itemService.createComment(author.getId(), item.getId(), newComment); + + assertNotNull(dto); + assertEquals("Great item!", dto.text()); + verify(commentRepository).save(any()); + } + + @Test + @DisplayName("createComment - should throw ItemCommentException when user has not booked") + void createComment_notBooked() { + User author = new User(2L, "Author", "author@example.com"); + NewCommentDto newComment = new NewCommentDto("Great item!"); + + when(userRepository.findById(author.getId())).thenReturn(Optional.of(author)); + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + when(bookingRepository.findByBookerIdAndItemIdAndStatusAndEndTimeIsBefore( + anyLong(), anyLong(), any(), any() + )).thenReturn(Optional.empty()); + + assertThrows(ItemCommentException.class, + () -> itemService.createComment(author.getId(), item.getId(), newComment)); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/request/ItemRequestMapperTest.java b/server/src/test/java/ru/practicum/shareit/request/ItemRequestMapperTest.java new file mode 100644 index 0000000..f5c3d9c --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/ItemRequestMapperTest.java @@ -0,0 +1,70 @@ +package ru.practicum.shareit.request; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.item.dto.ItemForRequestDto; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestWithResponsesDto; +import ru.practicum.shareit.request.dto.NewItemRequestDto; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class ItemRequestMapperTest { + + @Test + @DisplayName("toItemRequestWithResponseDto - should map ItemRequest and items to ItemRequestWithResponsesDto") + void toItemRequestWithResponseDto_ok() { + User requestor = new User(1L, "John", "john@example.com"); + ItemRequest request = new ItemRequest(2L, "Need drill", requestor, LocalDateTime.now()); + + User owner = new User(2L, "Owner", "owner@example.com"); + Item item = new Item(3L, "Drill", "Powerful drill", true, owner, null); + + ItemRequestWithResponsesDto dto = ItemRequestMapper.toItemRequestWithResponseDto(request, List.of(item)); + + assertEquals(request.getId(), dto.id()); + assertEquals(request.getDescription(), dto.description()); + assertEquals(request.getCreatedAt(), dto.created()); + assertNotNull(dto.items()); + assertEquals(1, dto.items().size()); + ItemForRequestDto itemDto = dto.items().getFirst(); + assertEquals(item.getId(), itemDto.id()); + assertEquals(item.getName(), itemDto.name()); + assertEquals(owner.getId(), itemDto.ownerId()); + } + + @Test + @DisplayName("toEntity - should map NewItemRequestDto to ItemRequest correctly") + void toEntity_ok() { + User requestor = new User(1L, "John", "john@example.com"); + NewItemRequestDto newRequestDto = new NewItemRequestDto("Need drill"); + LocalDateTime now = LocalDateTime.now(); + + ItemRequest request = ItemRequestMapper.toEntity(newRequestDto, requestor, now); + + assertNull(request.getId()); + assertEquals(newRequestDto.description(), request.getDescription()); + assertEquals(requestor, request.getRequestor()); + assertEquals(now, request.getCreatedAt()); + } + + @Test + @DisplayName("toItemRequestDto - should map ItemRequest to ItemRequestDto correctly") + void toItemRequestDto_ok() { + User requestor = new User(1L, "John", "john@example.com"); + ItemRequest request = new ItemRequest(2L, "Need drill", requestor, LocalDateTime.now()); + + ItemRequestDto dto = ItemRequestMapper.toItemRequestDto(request); + + assertEquals(request.getId(), dto.id()); + assertEquals(request.getDescription(), dto.description()); + assertEquals(requestor.getId(), dto.requestorId()); + assertEquals(request.getCreatedAt(), dto.created()); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceTest.java b/server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceTest.java new file mode 100644 index 0000000..b36a300 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceTest.java @@ -0,0 +1,126 @@ +package ru.practicum.shareit.request; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.data.domain.Sort; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestWithResponsesDto; +import ru.practicum.shareit.request.dto.NewItemRequestDto; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.request.repository.ItemRequestRepository; +import ru.practicum.shareit.request.service.ItemRequestServiceImpl; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class ItemRequestServiceTest { + + @Mock + private ItemRequestRepository requestRepository; + @Mock + private UserRepository userRepository; + @Mock + private ItemRepository itemRepository; + + @InjectMocks + private ItemRequestServiceImpl service; + + private User user; + private NewItemRequestDto newRequestDto; + private ItemRequest request; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + user = new User(1L, "User", "user@example.com"); + newRequestDto = new NewItemRequestDto("Need a drill"); + request = new ItemRequest(1L, "Need a drill", user, LocalDateTime.now()); + } + + @Test + @DisplayName("create - should save request successfully") + void create_ok() { + when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); + when(requestRepository.save(any())).thenReturn(request); + + ItemRequestDto dto = service.create(user.getId(), newRequestDto); + + assertNotNull(dto); + assertEquals(request.getId(), dto.id()); + verify(requestRepository).save(any()); + } + + @Test + @DisplayName("create - should throw NotFoundException when user not found") + void create_userNotFound() { + when(userRepository.findById(user.getId())).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> service.create(user.getId(), newRequestDto)); + } + + @Test + @DisplayName("getAllOfUser - should return requests with items") + void getAllOfUser_ok() { + when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); + when(requestRepository.findAllByRequestor_IdOrderByCreatedAtDesc(user.getId())) + .thenReturn(List.of(request)); + Item item = new Item(1L, "Drill", "Powerful drill", true, user, request); + when(itemRepository.findAllByRequest_IdIn(List.of(request.getId()))) + .thenReturn(List.of(item)); + + Collection result = service.getAllOfUser(user.getId()); + + assertEquals(1, result.size()); + verify(requestRepository).findAllByRequestor_IdOrderByCreatedAtDesc(user.getId()); + } + + @Test + @DisplayName("getAll - should return all requests sorted") + void getAll_ok() { + when(requestRepository.findAll(Mockito.any(Sort.class))) + .thenReturn(List.of(request)); + + Collection result = service.getAll(); + + assertEquals(1, result.size()); + verify(requestRepository).findAll(any(Sort.class)); + } + + @Test + @DisplayName("getById - should return request with items") + void getById_ok() { + when(requestRepository.findById(request.getId())).thenReturn(Optional.of(request)); + Item item = new Item(1L, "Drill", "Powerful drill", true, user, request); + when(itemRepository.findAllByRequest_Id(request.getId())).thenReturn(List.of(item)); + + ItemRequestWithResponsesDto dto = service.getById(request.getId()); + + assertNotNull(dto); + assertEquals(request.getId(), dto.id()); + verify(requestRepository).findById(request.getId()); + verify(itemRepository).findAllByRequest_Id(request.getId()); + } + + @Test + @DisplayName("getById - should throw NotFoundException when request not found") + void getById_notFound() { + when(requestRepository.findById(request.getId())).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> service.getById(request.getId())); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/user/UserMapperTest.java b/server/src/test/java/ru/practicum/shareit/user/UserMapperTest.java new file mode 100644 index 0000000..fd9cb91 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserMapperTest.java @@ -0,0 +1,61 @@ +package ru.practicum.shareit.user; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.user.dto.NewUserDto; +import ru.practicum.shareit.user.dto.UpdateUserDto; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.model.User; + +import static org.junit.jupiter.api.Assertions.*; + +class UserMapperTest { + + @Test + @DisplayName("toUserDto - should map User to UserDto correctly") + void toUserDto_ok() { + User user = new User(1L, "John Doe", "john@example.com"); + + UserDto dto = UserMapper.toUserDto(user); + + assertEquals(user.getId(), dto.id()); + assertEquals(user.getName(), dto.name()); + assertEquals(user.getEmail(), dto.email()); + } + + @Test + @DisplayName("toUser - should map NewUserDto to User correctly") + void toUser_ok() { + NewUserDto newUserDto = new NewUserDto("John Doe", "john@example.com"); + + User user = UserMapper.toUser(newUserDto); + + assertNull(user.getId()); + assertEquals(newUserDto.name(), user.getName()); + assertEquals(newUserDto.email(), user.getEmail()); + } + + @Test + @DisplayName("updateUser - should update User fields when UpdateUserDto has values") + void updateUser_ok() { + User user = new User(1L, "John", "john@example.com"); + UpdateUserDto update = new UpdateUserDto(null, "Jane", "jane@example.com"); + + User updated = UserMapper.updateUser(user, update); + + assertEquals("Jane", updated.getName()); + assertEquals("jane@example.com", updated.getEmail()); + } + + @Test + @DisplayName("updateUser - should not change User fields when UpdateUserDto has nulls") + void updateUser_partial() { + User user = new User(1L, "John", "john@example.com"); + UpdateUserDto update = new UpdateUserDto(1L, null, null); + + User updated = UserMapper.updateUser(user, update); + + assertEquals("John", updated.getName()); + assertEquals("john@example.com", updated.getEmail()); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/user/UserServiceTest.java b/server/src/test/java/ru/practicum/shareit/user/UserServiceTest.java new file mode 100644 index 0000000..1c3a3db --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserServiceTest.java @@ -0,0 +1,128 @@ +package ru.practicum.shareit.user; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.user.dto.NewUserDto; +import ru.practicum.shareit.user.dto.UpdateUserDto; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +class UserServiceTest { + + @Mock + private UserRepository userRepository; + + @InjectMocks + private UserServiceImpl userService; + + private User user; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + user = new User(); + user.setId(1L); + user.setName("John Doe"); + user.setEmail("john@example.com"); + } + + @Test + @DisplayName("findAll - should return list of users") + void findAll_ok() { + when(userRepository.findAll()).thenReturn(List.of(user)); + + List result = userService.findAll().stream().toList(); + + assertEquals(1, result.size()); + assertEquals("John Doe", result.get(0).name()); + verify(userRepository).findAll(); + } + + @Test + @DisplayName("findById - should return user when exists") + void findById_ok() { + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + + UserDto result = userService.findById(1L); + + assertEquals("John Doe", result.name()); + verify(userRepository).findById(1L); + } + + @Test + @DisplayName("findById - should throw NotFoundException when user does not exist") + void findById_notFound() { + when(userRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> userService.findById(1L)); + verify(userRepository).findById(1L); + } + + @Test + @DisplayName("save - should save and return user") + void save_ok() { + NewUserDto newUserDto = new NewUserDto("John Doe", "john@example.com"); + when(userRepository.save(any(User.class))).thenReturn(user); + + UserDto result = userService.save(newUserDto); + + assertEquals("John Doe", result.name()); + verify(userRepository).save(any(User.class)); + } + + @Test + @DisplayName("update - should update and return user") + void update_ok() { + UpdateUserDto updateUserDto = new UpdateUserDto(1L, "Jane Doe", "jane@example.com"); + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + when(userRepository.save(any(User.class))).thenReturn(user); + + UserDto result = userService.update(updateUserDto); + + assertEquals("Jane Doe", result.name()); + verify(userRepository).findById(1L); + verify(userRepository).save(any(User.class)); + } + + @Test + @DisplayName("update - should throw NotFoundException when user does not exist") + void update_notFound() { + UpdateUserDto updateUserDto = new UpdateUserDto(1L, "Jane Doe", "jane@example.com"); + when(userRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> userService.update(updateUserDto)); + verify(userRepository).findById(1L); + } + + @Test + @DisplayName("delete - should delete user when exists") + void delete_ok() { + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + + userService.delete(1L); + + verify(userRepository).deleteById(1L); + } + + @Test + @DisplayName("delete - should throw NotFoundException when user does not exist") + void delete_notFound() { + when(userRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> userService.delete(1L)); + verify(userRepository, never()).deleteById(anyLong()); + } +} From 1a94b8ef02f929d9042b324a69ee84412e99d99a Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Wed, 10 Dec 2025 07:39:07 +0300 Subject: [PATCH 20/23] feat: add more tests to booking and item services --- .../shareit/booking/BookingServiceTest.java | 66 ++++++++++++++++++ .../shareit/item/ItemServiceTest.java | 67 +++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingServiceTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingServiceTest.java index 8d21034..2e13279 100644 --- a/server/src/test/java/ru/practicum/shareit/booking/BookingServiceTest.java +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingServiceTest.java @@ -145,4 +145,70 @@ void getBookingById_noAccess() { assertThrows(ForbiddenAccessException.class, () -> bookingService.getBookingById(booking.getId(), other.getId())); } + + @Test + @DisplayName("getAllBookingsOfUser - ALL returns bookings") + void getAllBookingsOfUser_all() { + when(userRepository.findById(booker.getId())).thenReturn(Optional.of(booker)); + when(bookingRepository.findByBookerIdOrderByStartTimeDesc(booker.getId())) + .thenReturn(List.of(new Booking(1L, bookingDto.start(), bookingDto.end(), item, booker, BookingStatus.WAITING))); + + List bookings = bookingService.getAllBookingsOfUser(booker.getId(), BookingState.ALL); + + assertEquals(1, bookings.size()); + verify(bookingRepository).findByBookerIdOrderByStartTimeDesc(booker.getId()); + } + + @Test + @DisplayName("getAllBookingsOfUser - FUTURE returns bookings") + void getAllBookingsOfUser_future() { + when(userRepository.findById(booker.getId())).thenReturn(Optional.of(booker)); + when(bookingRepository.findByBookerIdAndStartTimeAfterOrderByStartTimeDesc(eq(booker.getId()), any(LocalDateTime.class))) + .thenReturn(List.of(new Booking(2L, bookingDto.start().plusDays(1), bookingDto.end().plusDays(1), item, booker, BookingStatus.WAITING))); + + List bookings = bookingService.getAllBookingsOfUser(booker.getId(), BookingState.FUTURE); + + assertEquals(1, bookings.size()); + verify(bookingRepository).findByBookerIdAndStartTimeAfterOrderByStartTimeDesc(eq(booker.getId()), any(LocalDateTime.class)); + } + + @Test + @DisplayName("getAllBookingsByOwner - PAST returns bookings") + void getAllBookingsByOwner_past() { + when(userRepository.findById(owner.getId())).thenReturn(Optional.of(owner)); + when(bookingRepository.findByItemOwnerIdAndEndTimeBeforeOrderByStartTimeDesc(eq(owner.getId()), any(LocalDateTime.class))) + .thenReturn(List.of(new Booking(3L, bookingDto.start().minusDays(2), bookingDto.end().minusDays(1), item, booker, BookingStatus.APPROVED))); + + List bookings = bookingService.getAllBookingsByOwner(owner.getId(), BookingState.PAST); + + assertEquals(1, bookings.size()); + verify(bookingRepository).findByItemOwnerIdAndEndTimeBeforeOrderByStartTimeDesc(eq(owner.getId()), any(LocalDateTime.class)); + } + + @Test + @DisplayName("getAllBookingsByOwner - empty list when no bookings") + void getAllBookingsByOwner_empty() { + when(userRepository.findById(owner.getId())).thenReturn(Optional.of(owner)); + when(bookingRepository.findByItemOwnerIdOrderByStartTimeDesc(owner.getId())).thenReturn(List.of()); + + List bookings = bookingService.getAllBookingsByOwner(owner.getId(), BookingState.ALL); + + assertTrue(bookings.isEmpty()); + } + + @Test + @DisplayName("getAllBookingsOfUser - throws NotFoundException if user not found") + void getAllBookingsOfUser_userNotFound() { + when(userRepository.findById(booker.getId())).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> bookingService.getAllBookingsOfUser(booker.getId(), BookingState.ALL)); + } + + @Test + @DisplayName("getAllBookingsByOwner - throws NotFoundException if user not found") + void getAllBookingsByOwner_userNotFound() { + when(userRepository.findById(owner.getId())).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> bookingService.getAllBookingsByOwner(owner.getId(), BookingState.ALL)); + } } diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemServiceTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemServiceTest.java index 94acd9d..c9d7315 100644 --- a/server/src/test/java/ru/practicum/shareit/item/ItemServiceTest.java +++ b/server/src/test/java/ru/practicum/shareit/item/ItemServiceTest.java @@ -22,6 +22,8 @@ import ru.practicum.shareit.user.repository.UserRepository; import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; @@ -142,4 +144,69 @@ void createComment_notBooked() { assertThrows(ItemCommentException.class, () -> itemService.createComment(author.getId(), item.getId(), newComment)); } + @Test + @DisplayName("getItemOfUserById - returns item with comments") + void getItemOfUserById_ok() { + when(userRepository.findById(owner.getId())).thenReturn(Optional.of(owner)); + when(itemRepository.findById(item.getId())).thenReturn(Optional.of(item)); + Comment comment = new Comment(1L, "Nice item", item, owner, LocalDateTime.now()); + when(commentRepository.findAllByItem_IdInOrderByCreatedAtDesc(List.of(item.getId()))) + .thenReturn(List.of(comment)); + + ItemWithBookingDto dto = itemService.getItemOfUserById(owner.getId(), item.getId()); + + assertNotNull(dto); + assertEquals(item.getId(), dto.id()); + assertNotNull(dto.comments()); + assertEquals(1, dto.comments().size()); + verify(itemRepository).findById(item.getId()); + verify(commentRepository).findAllByItem_IdInOrderByCreatedAtDesc(List.of(item.getId())); + } + + @Test + @DisplayName("getItemOfUserById - throws NotFoundException when item not found") + void getItemOfUserById_itemNotFound() { + when(userRepository.findById(owner.getId())).thenReturn(Optional.of(owner)); + when(itemRepository.findById(item.getId())).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> + itemService.getItemOfUserById(owner.getId(), item.getId())); + } + + @Test + @DisplayName("getAllItemsOfUser - returns items with bookings and comments") + void getAllItemsOfUser_ok() { + when(userRepository.findById(owner.getId())).thenReturn(Optional.of(owner)); + when(itemRepository.findAllByOwnerId(owner.getId())).thenReturn(List.of(item)); + + Booking booking = new Booking(1L, LocalDateTime.now().minusDays(1), LocalDateTime.now().plusDays(1), + item, owner, BookingStatus.APPROVED); + when(bookingRepository.findAllByItem_IdInAndStatus(List.of(item.getId()), BookingStatus.APPROVED)) + .thenReturn(List.of(booking)); + + Comment comment = new Comment(1L, "Great item", item, owner, LocalDateTime.now()); + when(commentRepository.findAllByItem_IdInOrderByCreatedAtDesc(List.of(item.getId()))) + .thenReturn(List.of(comment)); + + Collection items = itemService.getAllItemsOfUser(owner.getId()); + + assertNotNull(items); + assertEquals(1, items.size()); + ItemWithBookingDto dto = items.iterator().next(); + assertEquals(item.getId(), dto.id()); + assertNotNull(dto.comments()); + assertEquals(1, dto.comments().size()); + + verify(itemRepository, times(2)).findAllByOwnerId(owner.getId()); + verify(bookingRepository).findAllByItem_IdInAndStatus(List.of(item.getId()), BookingStatus.APPROVED); + verify(commentRepository).findAllByItem_IdInOrderByCreatedAtDesc(List.of(item.getId())); + } + + @Test + @DisplayName("getAllItemsOfUser - throws NotFoundException when user not found") + void getAllItemsOfUser_userNotFound() { + when(userRepository.findById(owner.getId())).thenReturn(Optional.empty()); + assertThrows(NotFoundException.class, () -> + itemService.getAllItemsOfUser(owner.getId())); + } } From b5b6d576fa96c487adc75ff7eb8216fa0909fbc5 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Thu, 11 Dec 2025 11:57:44 +0300 Subject: [PATCH 21/23] feat: add dockerignore --- .dockerignore | 8 ++++++++ .../{main => test}/resources/application-test.properties | 0 2 files changed, 8 insertions(+) create mode 100644 .dockerignore rename server/src/{main => test}/resources/application-test.properties (100%) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..eeab35a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +# Ignore everything by default +* + +# Include what we need +!**/src +!.mvn +!**/pom.xml +!mvnw diff --git a/server/src/main/resources/application-test.properties b/server/src/test/resources/application-test.properties similarity index 100% rename from server/src/main/resources/application-test.properties rename to server/src/test/resources/application-test.properties From 477640ad88104698870fa89f5a89568205a58af3 Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Thu, 11 Dec 2025 11:58:11 +0300 Subject: [PATCH 22/23] feat: add integration tests for services --- .../BookingServiceIntegrationTest.java | 116 +++++++++++++++++ .../item/ItemServiceIntegrationTest.java | 114 +++++++++++++++++ .../ItemRequestServiceIntegrationTest.java | 117 ++++++++++++++++++ .../user/UserServiceIntegrationTest.java | 56 +++++++++ .../resources/application-test.properties | 1 - 5 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 server/src/test/java/ru/practicum/shareit/booking/BookingServiceIntegrationTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/item/ItemServiceIntegrationTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceIntegrationTest.java create mode 100644 server/src/test/java/ru/practicum/shareit/user/UserServiceIntegrationTest.java diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingServiceIntegrationTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingServiceIntegrationTest.java new file mode 100644 index 0000000..ba9d7e2 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingServiceIntegrationTest.java @@ -0,0 +1,116 @@ +package ru.practicum.shareit.booking; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; +import ru.practicum.shareit.booking.model.BookingStatus; +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 java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class BookingServiceIntegrationTest { + + @Autowired + private BookingService bookingService; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ItemRepository itemRepository; + + @Autowired + private BookingRepository bookingRepository; + + private User booker; + private User owner; + private Item item; + + @BeforeAll + void setup() { + owner = new User(); + owner.setName("Owner"); + owner.setEmail("owner@example.com"); + owner = userRepository.save(owner); + + booker = new User(); + booker.setName("Booker"); + booker.setEmail("booker@example.com"); + booker = userRepository.save(booker); + + item = new Item(); + item.setName("Drill"); + item.setDescription("Electric drill"); + item.setAvailable(true); + item.setOwner(owner); + item = itemRepository.save(item); + } + + @Test + @DisplayName("Should create a new booking successfully") + void testCreateBooking() { + BookingDto bookingDto = new BookingDto( + item.getId(), + LocalDateTime.now().plusDays(1), + LocalDateTime.now().plusDays(2) + ); + + BookingResponseDto response = bookingService.createBooking(booker.getId(), bookingDto); + + assertNotNull(response.id()); + assertEquals(item.getId(), response.item().id()); + assertEquals(booker.getId(), response.booker().id()); + assertEquals(BookingStatus.WAITING, response.status()); + } + + @Test + @DisplayName("Should approve a booking by item owner") + void testApproveBooking() { + BookingDto bookingDto = new BookingDto( + item.getId(), + LocalDateTime.now().plusDays(1), + LocalDateTime.now().plusDays(2) + ); + BookingResponseDto booking = bookingService.createBooking(booker.getId(), bookingDto); + + BookingResponseDto approvedBooking = bookingService.approveBooking(booking.id(), true, owner.getId()); + + assertEquals(BookingStatus.APPROVED, approvedBooking.status()); + } + + @Test + @DisplayName("Should retrieve all bookings of a user") + void testGetAllBookingsOfUser() { + bookingService.createBooking(booker.getId(), new BookingDto( + item.getId(), + LocalDateTime.now().minusDays(3), + LocalDateTime.now().minusDays(2) + )); + bookingService.createBooking(booker.getId(), new BookingDto( + item.getId(), + LocalDateTime.now().plusDays(1), + LocalDateTime.now().plusDays(2) + )); + + List allBookings = bookingService.getAllBookingsOfUser(booker.getId(), BookingState.ALL); + + assertFalse(allBookings.isEmpty()); + assertTrue(allBookings.stream().anyMatch(b -> b.booker().id().equals(booker.getId()))); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemServiceIntegrationTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemServiceIntegrationTest.java new file mode 100644 index 0000000..2717a88 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/ItemServiceIntegrationTest.java @@ -0,0 +1,114 @@ +package ru.practicum.shareit.item; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +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.*; +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.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ItemServiceIntegrationTest { + + @Autowired + private ItemService itemService; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ItemRepository itemRepository; + + @Autowired + private BookingRepository bookingRepository; + + @Autowired + private CommentRepository commentRepository; + + private User user; + private Item item; + + @BeforeAll + void setup() { + user = new User(); + user.setName("John Doe"); + user.setEmail("john@example.com"); + user = userRepository.save(user); + + item = new Item(); + item.setName("Drill"); + item.setDescription("Electric drill"); + item.setAvailable(true); + item.setOwner(user); + item = itemRepository.save(item); + } + + @Test + @DisplayName("Should save a new item successfully") + void testSaveItem() { + NewItemDto newItemDto = new NewItemDto("Hammer", "Heavy hammer", true, null); + ItemDto savedItem = itemService.saveItem(user.getId(), newItemDto); + + assertNotNull(savedItem.id()); + assertEquals("Hammer", savedItem.name()); + assertEquals("Heavy hammer", savedItem.description()); + assertTrue(savedItem.available()); + + Item itemFromDb = itemRepository.findById(savedItem.id()).orElseThrow(); + assertEquals("Hammer", itemFromDb.getName()); + } + + @Test + @DisplayName("Should retrieve all items of a user") + void testGetAllItemsOfUser() { + Collection items = itemService.getAllItemsOfUser(user.getId()); + + assertFalse(items.isEmpty()); + ItemWithBookingDto itemDto = items.iterator().next(); + assertEquals(item.getId(), itemDto.id()); + assertEquals(item.getName(), itemDto.name()); + assertEquals(item.getDescription(), itemDto.description()); + } + + @Test + @DisplayName("Should create a comment for an item after booking") + void testCreateComment() { + Booking booking = new Booking(); + booking.setItem(item); + booking.setBooker(user); + booking.setStartTime(LocalDateTime.now().minusDays(2)); + booking.setEndTime(LocalDateTime.now().minusDays(1)); + booking.setStatus(BookingStatus.APPROVED); + bookingRepository.save(booking); + + NewCommentDto newComment = new NewCommentDto("Great tool!"); + CommentDto commentDto = itemService.createComment(user.getId(), item.getId(), newComment); + + assertNotNull(commentDto.id()); + assertEquals("Great tool!", commentDto.text()); + assertEquals(user.getName(), commentDto.authorName()); + assertFalse(commentDto.created().isAfter(LocalDateTime.now())); + + List comments = commentRepository.findAllByItem_IdInOrderByCreatedAtDesc(List.of(item.getId())); + assertTrue(comments.stream().anyMatch(c -> c.getText().equals("Great tool!"))); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceIntegrationTest.java b/server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceIntegrationTest.java new file mode 100644 index 0000000..aacbeb5 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceIntegrationTest.java @@ -0,0 +1,117 @@ +package ru.practicum.shareit.request; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.item.dto.ItemForRequestDto; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestWithResponsesDto; +import ru.practicum.shareit.request.dto.NewItemRequestDto; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.request.repository.ItemRequestRepository; +import ru.practicum.shareit.request.service.ItemRequestService; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@Transactional +class ItemRequestServiceIntegrationTest { + + @Autowired + private ItemRequestService itemRequestService; + + @Autowired + private ItemRequestRepository itemRequestRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ItemRepository itemRepository; + + @Test + @DisplayName("create(): should create a new item request for the user") + void testCreateRequest() { + User user = saveUser("Alex", "alex@mail.com"); + NewItemRequestDto dto = new NewItemRequestDto("Need a drill"); + + ItemRequestDto saved = itemRequestService.create(user.getId(), dto); + + assertNotNull(saved.id()); + assertEquals("Need a drill", saved.description()); + assertEquals(user.getId(), saved.requestorId()); + + ItemRequest requestInDb = itemRequestRepository.findById(saved.id()) + .orElseThrow(); + assertEquals("Need a drill", requestInDb.getDescription()); + assertEquals(user.getId(), requestInDb.getRequestor().getId()); + assertNotNull(requestInDb.getCreatedAt()); + } + + @Test + @DisplayName("getAll(): should return all item requests sorted by createdAt DESC") + void testGetAll() { + User user = saveUser("Bob", "bob@mail.com"); + + ItemRequest r1 = itemRequestRepository.save( + new ItemRequest(null, "Request A", user, LocalDateTime.now().minusDays(1)) + ); + ItemRequest r2 = itemRequestRepository.save( + new ItemRequest(null, "Request B", user, LocalDateTime.now()) + ); + + Collection result = itemRequestService.getAll(); + + assertEquals(2, result.size()); + + List list = result.stream().toList(); + + assertEquals(r2.getId(), list.get(0).id()); + assertEquals(r1.getId(), list.get(1).id()); + } + + @Test + @DisplayName("getById(): should return item request with its responses") + void testGetById() { + User user = saveUser("Chris", "chris@mail.com"); + + ItemRequest request = itemRequestRepository.save( + new ItemRequest(null, "Need a ladder", user, LocalDateTime.now()) + ); + + Item item1 = itemRepository.save( + new Item(null, "Ladder", "Good ladder", true, user, request) + ); + + Item item2 = itemRepository.save( + new Item(null, "Small Ladder", "Compact", true, user, request) + ); + + ItemRequestWithResponsesDto result = itemRequestService.getById(request.getId()); + + assertEquals(request.getId(), result.id()); + assertEquals("Need a ladder", result.description()); + assertEquals(2, result.items().size()); + + List itemNames = result.items().stream() + .map(ItemForRequestDto::name) + .toList(); + + assertTrue(itemNames.contains("Ladder")); + assertTrue(itemNames.contains("Small Ladder")); + } + + private User saveUser(String name, String email) { + return userRepository.save(new User(null, name, email)); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/user/UserServiceIntegrationTest.java b/server/src/test/java/ru/practicum/shareit/user/UserServiceIntegrationTest.java new file mode 100644 index 0000000..c1664ef --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserServiceIntegrationTest.java @@ -0,0 +1,56 @@ +package ru.practicum.shareit.user; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.user.dto.NewUserDto; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@SpringBootTest +@Transactional +class UserServiceIntegrationTest { + + @Autowired + private UserService userService; + + @Autowired + private UserRepository userRepository; + + @Test + @DisplayName("save(): should persist a new user and return correct DTO") + void testSaveUser() { + NewUserDto newUser = new NewUserDto("Alex", "alex@mail.com"); + + UserDto saved = userService.save(newUser); + + assertNotNull(saved.id()); + assertEquals("Alex", saved.name()); + assertEquals("alex@mail.com", saved.email()); + + User userInDb = userRepository.findById(saved.id()) + .orElseThrow(); + + assertEquals("Alex", userInDb.getName()); + assertEquals("alex@mail.com", userInDb.getEmail()); + } + + @Test + @DisplayName("findById(): should return the correct user from the database") + void testFindById() { + User user = new User(null, "Maria", "maria@mail.com"); + user = userRepository.save(user); + + UserDto found = userService.findById(user.getId()); + + assertEquals(user.getId(), found.id()); + assertEquals("Maria", found.name()); + assertEquals("maria@mail.com", found.email()); + } +} diff --git a/server/src/test/resources/application-test.properties b/server/src/test/resources/application-test.properties index 09fc2b7..abd24f1 100644 --- a/server/src/test/resources/application-test.properties +++ b/server/src/test/resources/application-test.properties @@ -4,7 +4,6 @@ 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.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect spring.sql.init.mode=never spring.datasource.driver-class-name=org.h2.Driver From 86ad366e5a1dac4a5812e018a7706b7f33bca10b Mon Sep 17 00:00:00 2001 From: Ilia Egorov Date: Thu, 11 Dec 2025 16:59:49 +0300 Subject: [PATCH 23/23] feat: add lifecycle per class annotation --- .../shareit/request/ItemRequestServiceIntegrationTest.java | 2 ++ .../ru/practicum/shareit/user/UserServiceIntegrationTest.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceIntegrationTest.java b/server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceIntegrationTest.java index aacbeb5..8a7abef 100644 --- a/server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceIntegrationTest.java +++ b/server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceIntegrationTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; @@ -25,6 +26,7 @@ @SpringBootTest @Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class ItemRequestServiceIntegrationTest { @Autowired diff --git a/server/src/test/java/ru/practicum/shareit/user/UserServiceIntegrationTest.java b/server/src/test/java/ru/practicum/shareit/user/UserServiceIntegrationTest.java index c1664ef..6b89a44 100644 --- a/server/src/test/java/ru/practicum/shareit/user/UserServiceIntegrationTest.java +++ b/server/src/test/java/ru/practicum/shareit/user/UserServiceIntegrationTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; @@ -15,6 +16,7 @@ @SpringBootTest @Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class UserServiceIntegrationTest { @Autowired