diff --git a/gateway/src/main/java/ru/practicum/shareit/api/RequestHttpHeaders.java b/gateway/src/main/java/ru/practicum/shareit/api/RequestHttpHeaders.java new file mode 100644 index 00000000..f5684d5b --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/api/RequestHttpHeaders.java @@ -0,0 +1,8 @@ +package ru.practicum.shareit.api; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class RequestHttpHeaders { + public static final String USER_ID = "X-Sharer-User-Id"; +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/api/ValidateCreateRequest.java b/gateway/src/main/java/ru/practicum/shareit/api/ValidateCreateRequest.java new file mode 100644 index 00000000..c3431165 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/api/ValidateCreateRequest.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.api; + +public interface ValidateCreateRequest { +} diff --git a/gateway/src/main/java/ru/practicum/shareit/api/ValidateUpdateRequest.java b/gateway/src/main/java/ru/practicum/shareit/api/ValidateUpdateRequest.java new file mode 100644 index 00000000..10d8d0ee --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/api/ValidateUpdateRequest.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.api; + +public interface ValidateUpdateRequest { +} diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java b/gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java deleted file mode 100644 index 471c44f5..00000000 --- a/gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java +++ /dev/null @@ -1,48 +0,0 @@ -package ru.practicum.shareit.booking; - -import java.util.Map; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.http.ResponseEntity; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.stereotype.Service; -import org.springframework.web.util.DefaultUriBuilderFactory; - -import ru.practicum.shareit.booking.dto.BookItemRequestDto; -import ru.practicum.shareit.booking.dto.BookingState; -import ru.practicum.shareit.client.BaseClient; - -@Service -public class BookingClient extends BaseClient { - private static final String API_PREFIX = "/bookings"; - - @Autowired - public BookingClient(@Value("${shareit-server.url}") String serverUrl, RestTemplateBuilder builder) { - super( - builder - .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + API_PREFIX)) - .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) - .build() - ); - } - - public ResponseEntity getBookings(long userId, BookingState state, Integer from, Integer size) { - Map parameters = Map.of( - "state", state.name(), - "from", from, - "size", size - ); - return get("?state={state}&from={from}&size={size}", userId, parameters); - } - - - public ResponseEntity bookItem(long userId, BookItemRequestDto requestDto) { - return post("", userId, requestDto); - } - - public ResponseEntity getBooking(long userId, Long bookingId) { - return get("/" + bookingId, userId); - } -} diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java b/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java deleted file mode 100644 index 62bdce2f..00000000 --- a/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ /dev/null @@ -1,55 +0,0 @@ -package ru.practicum.shareit.booking; - -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; - -import jakarta.validation.Valid; -import jakarta.validation.constraints.Positive; -import jakarta.validation.constraints.PositiveOrZero; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import ru.practicum.shareit.booking.dto.BookItemRequestDto; -import ru.practicum.shareit.booking.dto.BookingState; - - -@Controller -@RequestMapping(path = "/bookings") -@RequiredArgsConstructor -@Slf4j -@Validated -public class BookingController { - private final BookingClient bookingClient; - - @GetMapping - public ResponseEntity getBookings(@RequestHeader("X-Sharer-User-Id") long userId, - @RequestParam(name = "state", defaultValue = "all") String stateParam, - @PositiveOrZero @RequestParam(name = "from", defaultValue = "0") Integer from, - @Positive @RequestParam(name = "size", defaultValue = "10") Integer size) { - BookingState state = BookingState.from(stateParam) - .orElseThrow(() -> new IllegalArgumentException("Unknown state: " + stateParam)); - log.info("Get booking with state {}, userId={}, from={}, size={}", stateParam, userId, from, size); - return bookingClient.getBookings(userId, state, from, size); - } - - @PostMapping - public ResponseEntity bookItem(@RequestHeader("X-Sharer-User-Id") long userId, - @RequestBody @Valid BookItemRequestDto requestDto) { - log.info("Creating booking {}, userId={}", requestDto, userId); - return bookingClient.bookItem(userId, requestDto); - } - - @GetMapping("/{bookingId}") - public ResponseEntity getBooking(@RequestHeader("X-Sharer-User-Id") long userId, - @PathVariable Long bookingId) { - log.info("Get booking {}, userId={}", bookingId, userId); - return bookingClient.getBooking(userId, bookingId); - } -} diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/client/BookingClient.java b/gateway/src/main/java/ru/practicum/shareit/booking/client/BookingClient.java new file mode 100644 index 00000000..b0202e58 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/client/BookingClient.java @@ -0,0 +1,55 @@ +package ru.practicum.shareit.booking.client; + +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.util.DefaultUriBuilderFactory; + +import ru.practicum.shareit.booking.dto.BookingSaveDto; +import ru.practicum.shareit.booking.dto.BookingState; +import ru.practicum.shareit.client.BaseClient; + +@Service +public class BookingClient extends BaseClient { + private static final String API_PREFIX = "/bookings"; + private static final String BOOKING_ID_PATH = "/{bookingId}"; + private static final String BOOKING_PATH = BOOKING_ID_PATH + "?approved={approved}"; + private static final String ALL_USER_BOOKINGS_PATH = "?state={state}&from={from}&size={size}"; + private static final String ALL_USER_ITEMS_BOOKINGS_PATH = "/owner/?state={state}"; + + @Autowired + public BookingClient(@Value("${shareit-server.url}") String serverUrl, RestTemplateBuilder builder) { + super(builder.uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + API_PREFIX)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build()); + } + + public ResponseEntity addBooking(Integer userId, BookingSaveDto bookingSaveDto) { + return post("", userId, bookingSaveDto); + } + + public ResponseEntity manageBooking(Integer userId, Integer bookingId, Boolean approved) { + Map uriVariables = Map.of("bookingId",bookingId,"approved", approved); + return patch(BOOKING_PATH, userId, uriVariables); + } + + public ResponseEntity getBooking(Integer userId, Integer bookingId) { + Map uriVariables = Map.of("bookingId", bookingId); + return get(BOOKING_ID_PATH, userId, uriVariables); + } + + public ResponseEntity getAllUserBookings(Integer userId, BookingState state, Integer from, Integer size) { + Map uriVariables = Map.of("state", state.name(), "from", from, "size", size); + return get(ALL_USER_BOOKINGS_PATH, userId, uriVariables); + } + + public ResponseEntity getAllUserItemsBookings(Integer userId, BookingState state) { + Map uriVariables = Map.of("state", state.name()); + return get(ALL_USER_ITEMS_BOOKINGS_PATH, userId, uriVariables); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java b/gateway/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java new file mode 100644 index 00000000..8a253b8c --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java @@ -0,0 +1,68 @@ +package ru.practicum.shareit.booking.controller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.api.RequestHttpHeaders; +import ru.practicum.shareit.booking.client.BookingClient; +import ru.practicum.shareit.booking.dto.BookingSaveDto; +import ru.practicum.shareit.booking.dto.BookingState; +import ru.practicum.shareit.exceptions.NotValidException; + + +@Controller +@RequestMapping(path = "/bookings") +@RequiredArgsConstructor +@Slf4j +@Validated +public class BookingController { + private final BookingClient bookingClient; + + @PostMapping + public ResponseEntity addBooking(@RequestHeader(RequestHttpHeaders.USER_ID) Integer userId, + @RequestBody @Valid BookingSaveDto bookingSaveDto) { + return bookingClient.addBooking(userId, bookingSaveDto); + } + + @PatchMapping("/{bookingId}") + public ResponseEntity manageBooking(@RequestHeader(RequestHttpHeaders.USER_ID) Integer userId, + @PathVariable Integer bookingId, + @RequestParam Boolean approved) { + return bookingClient.manageBooking(userId, bookingId, approved); + } + + @GetMapping("/{bookingId}") + public ResponseEntity getBooking(@RequestHeader(RequestHttpHeaders.USER_ID) Integer userId, + @PathVariable Integer bookingId) { + return bookingClient.getBooking(userId, bookingId); + } + + @GetMapping + public ResponseEntity getAllUserBookings(@RequestHeader(RequestHttpHeaders.USER_ID) Integer userId, + @RequestParam(defaultValue = "all") String state, + @RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + BookingState bookingState = getBookingState(state); + return bookingClient.getAllUserBookings(userId, bookingState, from, size); + } + + @GetMapping("/owner") + public ResponseEntity getAllUserItemsBookings(@RequestHeader(RequestHttpHeaders.USER_ID) Integer userId, + @RequestParam(defaultValue = "all") String state) { + BookingState bookingState = getBookingState(state); + + return bookingClient.getAllUserItemsBookings(userId, bookingState); + } + + private BookingState getBookingState(String state) { + return BookingState.from(state) + .orElseThrow(() -> new NotValidException(BookingState.class, state + " not valid")); + } + +} diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookItemRequestDto.java b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookItemRequestDto.java deleted file mode 100644 index 738f5c08..00000000 --- a/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookItemRequestDto.java +++ /dev/null @@ -1,20 +0,0 @@ -package ru.practicum.shareit.booking.dto; - -import java.time.LocalDateTime; - -import jakarta.validation.constraints.Future; -import jakarta.validation.constraints.FutureOrPresent; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -@AllArgsConstructor -public class BookItemRequestDto { - private long itemId; - @FutureOrPresent - private LocalDateTime start; - @Future - private LocalDateTime end; -} diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingSaveDto.java b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingSaveDto.java new file mode 100644 index 00000000..26cc5497 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingSaveDto.java @@ -0,0 +1,21 @@ +package ru.practicum.shareit.booking.dto; + +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.validation.DateTimeStartBeforeEnd; + +import java.time.LocalDateTime; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +@DateTimeStartBeforeEnd +public class BookingSaveDto { + @NotNull + Integer itemId; + @FutureOrPresent + LocalDateTime start; + LocalDateTime end; +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingState.java b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingState.java index 7bfa1646..27938806 100644 --- a/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingState.java +++ b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingState.java @@ -3,25 +3,20 @@ import java.util.Optional; public enum BookingState { - // Все - ALL, - // Текущие - CURRENT, - // Будущие - FUTURE, - // Завершенные - PAST, - // Отклоненные - REJECTED, - // Ожидающие подтверждения - WAITING; - public static Optional from(String stringState) { - for (BookingState state : values()) { - if (state.name().equalsIgnoreCase(stringState)) { - return Optional.of(state); - } - } - return Optional.empty(); - } + ALL, + CURRENT, + FUTURE, + PAST, + REJECTED, + WAITING; + + public static Optional from(String stringState) { + for (BookingState state : values()) { + if (state.name().equalsIgnoreCase(stringState)) { + return Optional.of(state); + } + } + return Optional.empty(); + } } diff --git a/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java b/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java index 1a2d33a3..ad0d6e12 100644 --- a/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java +++ b/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java @@ -1,17 +1,13 @@ package ru.practicum.shareit.client; -import java.util.List; -import java.util.Map; - -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.lang.Nullable; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestTemplate; +import java.util.List; +import java.util.Map; + public class BaseClient { protected final RestTemplate rest; @@ -23,11 +19,11 @@ protected ResponseEntity get(String path) { return get(path, null, null); } - protected ResponseEntity get(String path, long userId) { + protected ResponseEntity get(String path, int userId) { return get(path, userId, null); } - protected ResponseEntity get(String path, Long userId, @Nullable Map parameters) { + protected ResponseEntity get(String path, Integer userId, @Nullable Map parameters) { return makeAndSendRequest(HttpMethod.GET, path, userId, parameters, null); } @@ -35,19 +31,19 @@ protected ResponseEntity post(String path, T body) { return post(path, null, null, body); } - protected ResponseEntity post(String path, long userId, T body) { + protected ResponseEntity post(String path, int userId, T body) { return post(path, userId, null, body); } - protected ResponseEntity post(String path, Long userId, @Nullable Map parameters, T body) { + protected ResponseEntity post(String path, Integer userId, @Nullable Map parameters, T body) { return makeAndSendRequest(HttpMethod.POST, path, userId, parameters, body); } - protected ResponseEntity put(String path, long userId, T body) { + protected ResponseEntity put(String path, int userId, T body) { return put(path, userId, null, body); } - protected ResponseEntity put(String path, long userId, @Nullable Map parameters, T body) { + protected ResponseEntity put(String path, int userId, @Nullable Map parameters, T body) { return makeAndSendRequest(HttpMethod.PUT, path, userId, parameters, body); } @@ -55,15 +51,20 @@ protected ResponseEntity patch(String path, T body) { return patch(path, null, null, body); } - protected ResponseEntity patch(String path, long userId) { + protected ResponseEntity patch(String path, int userId) { return patch(path, userId, null, null); } - protected ResponseEntity patch(String path, long userId, T body) { + protected ResponseEntity patch(String path, int userId, T body) { return patch(path, userId, null, body); } - protected ResponseEntity patch(String path, Long userId, @Nullable Map parameters, T body) { + protected ResponseEntity patch(String path, int userId, @Nullable Map parameters) { + return patch(path, userId, parameters, null); + } + + protected ResponseEntity patch(String path, Integer userId, + @Nullable Map parameters, T body) { return makeAndSendRequest(HttpMethod.PATCH, path, userId, parameters, body); } @@ -71,15 +72,15 @@ protected ResponseEntity delete(String path) { return delete(path, null, null); } - protected ResponseEntity delete(String path, long userId) { + protected ResponseEntity delete(String path, int userId) { return delete(path, userId, null); } - protected ResponseEntity delete(String path, Long userId, @Nullable Map parameters) { + protected ResponseEntity delete(String path, Integer userId, @Nullable Map parameters) { return makeAndSendRequest(HttpMethod.DELETE, path, userId, parameters, null); } - private ResponseEntity makeAndSendRequest(HttpMethod method, String path, Long userId, @Nullable Map parameters, @Nullable T body) { + private ResponseEntity makeAndSendRequest(HttpMethod method, String path, Integer userId, @Nullable Map parameters, @Nullable T body) { HttpEntity requestEntity = new HttpEntity<>(body, defaultHeaders(userId)); ResponseEntity shareitServerResponse; @@ -95,7 +96,7 @@ private ResponseEntity makeAndSendRequest(HttpMethod method, String return prepareGatewayResponse(shareitServerResponse); } - private HttpHeaders defaultHeaders(Long userId) { + private HttpHeaders defaultHeaders(Integer userId) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(List.of(MediaType.APPLICATION_JSON)); diff --git a/gateway/src/main/java/ru/practicum/shareit/exceptions/ErrorResponseMessage.java b/gateway/src/main/java/ru/practicum/shareit/exceptions/ErrorResponseMessage.java new file mode 100644 index 00000000..cc0ddb74 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/exceptions/ErrorResponseMessage.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.exceptions; + +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@RequiredArgsConstructor +public class ErrorResponseMessage { + private final String error; + private final String message; +} diff --git a/gateway/src/main/java/ru/practicum/shareit/exceptions/GatewayExceptionResolver.java b/gateway/src/main/java/ru/practicum/shareit/exceptions/GatewayExceptionResolver.java new file mode 100644 index 00000000..27fa29f5 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/exceptions/GatewayExceptionResolver.java @@ -0,0 +1,59 @@ +package ru.practicum.shareit.exceptions; + +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.apache.coyote.BadRequestException; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingRequestHeaderException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.stream.Collectors; + +@Slf4j(topic = "ExceptionResolver") +@RestControllerAdvice +@ControllerAdvice +public class GatewayExceptionResolver { + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(value = HttpStatus.BAD_REQUEST) + public ErrorResponseMessage handleValidationException(MethodArgumentNotValidException e) { + String message = e.getBindingResult() + .getFieldErrors() + .stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining(",")); + return new ErrorResponseMessage("Validation Error", message); + } + + @ExceptionHandler(BadRequestException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponseMessage handleBadRequestException(BadRequestException e) { + log.error(e.getMessage()); + return new ErrorResponseMessage("Произошла ошибка входных данных", e.getMessage()); + } + + @ExceptionHandler(MissingRequestHeaderException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponseMessage handleMissingRequestHeaderException(MissingRequestHeaderException e) { + log.error(e.getMessage()); + return new ErrorResponseMessage("Не передан Header X-Sharer-User-Id", e.getMessage()); + } + + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorResponseMessage handleConstraintViolationException(ConstraintViolationException e) { + return new ErrorResponseMessage("Constraint violation", e.getMessage()); + } + + @ExceptionHandler(NotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponseMessage notValidExceptionHandler(NotValidException e) { + log.error("not valid exception - {} ({})", e.getMessage(), e.getStackTrace()[0].toString()); + return new ErrorResponseMessage("BadRequest", e.getMessage()); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/exceptions/NotValidException.java b/gateway/src/main/java/ru/practicum/shareit/exceptions/NotValidException.java new file mode 100644 index 00000000..0b39fb5b --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/exceptions/NotValidException.java @@ -0,0 +1,8 @@ +package ru.practicum.shareit.exceptions; + +public class NotValidException extends RuntimeException { + + public NotValidException(Class entity, String reason) { + super(entity.getSimpleName() + " " + reason); + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/item/client/ItemClient.java b/gateway/src/main/java/ru/practicum/shareit/item/client/ItemClient.java new file mode 100644 index 00000000..fb8436ad --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/client/ItemClient.java @@ -0,0 +1,55 @@ +package ru.practicum.shareit.item.client; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.util.DefaultUriBuilderFactory; +import ru.practicum.shareit.client.BaseClient; +import ru.practicum.shareit.item.dto.CommentSaveDto; +import ru.practicum.shareit.item.dto.ItemSaveDto; + +import java.util.Map; + +@Service +public class ItemClient extends BaseClient { + private static final String ADD_COMMENT = "/{itemId}/comment"; + private static final String SEARCH_PATH = "/search?text="; + private static final String ITEMS_PATH = "/items"; + + @Autowired + public ItemClient(@Value("${shareit-server.url}") String serverUrl, RestTemplateBuilder builder) { + super(builder.uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + ITEMS_PATH)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build()); + } + + public ResponseEntity addItem(Integer userId, ItemSaveDto itemSaveDto) { + return post("", userId, itemSaveDto); + } + + public ResponseEntity addComment(Integer userId, Integer itemId, CommentSaveDto commentSaveDto) { + Map uriVariables = Map.of("itemId", itemId); + return post(ADD_COMMENT, userId, uriVariables, commentSaveDto); + } + + public ResponseEntity updateItem(Integer userId, Integer itemId, ItemSaveDto itemSaveDto) { + String path = "/" + itemId.toString(); + return patch(path, userId, itemSaveDto); + } + + public ResponseEntity getItem(Integer itemId) { + String path = "/" + itemId.toString(); + return get(path); + } + + public ResponseEntity getAllOwnerItems(Integer userId) { + return get("", userId); + } + + public ResponseEntity searchItems(String text) { + return get(SEARCH_PATH + text); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/item/controller/ItemController.java b/gateway/src/main/java/ru/practicum/shareit/item/controller/ItemController.java new file mode 100644 index 00000000..e2e16a21 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/controller/ItemController.java @@ -0,0 +1,61 @@ +package ru.practicum.shareit.item.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.api.RequestHttpHeaders; +import ru.practicum.shareit.api.ValidateCreateRequest; +import ru.practicum.shareit.api.ValidateUpdateRequest; +import ru.practicum.shareit.item.client.ItemClient; +import ru.practicum.shareit.item.dto.CommentSaveDto; +import ru.practicum.shareit.item.dto.ItemSaveDto; + +import java.util.List; + +@Controller +@RequestMapping("/items") +@RequiredArgsConstructor +@Validated +public class ItemController { + + private final ItemClient itemClient; + + @PostMapping + public ResponseEntity addItem(@RequestHeader(RequestHttpHeaders.USER_ID) Integer userId, + @RequestBody @Validated(ValidateCreateRequest.class) ItemSaveDto itemSaveDto) { + return itemClient.addItem(userId, itemSaveDto); + } + + @PostMapping("/{itemId}/comment") + public ResponseEntity addComment(@RequestHeader(RequestHttpHeaders.USER_ID) Integer userId, + @PathVariable Integer itemId, + @RequestBody @Validated(ValidateCreateRequest.class) + CommentSaveDto commentSaveDto) { + return itemClient.addComment(userId, itemId, commentSaveDto); + } + + @PatchMapping("/{itemId}") + public ResponseEntity updateItem(@RequestHeader(RequestHttpHeaders.USER_ID) Integer userId, + @PathVariable Integer itemId, + @RequestBody @Validated(ValidateUpdateRequest.class) + ItemSaveDto itemSaveDto) { + return itemClient.updateItem(userId, itemId, itemSaveDto); + } + + @GetMapping("/{itemId}") + public ResponseEntity getItem(@PathVariable Integer itemId) { + return itemClient.getItem(itemId); + } + + @GetMapping + public ResponseEntity getAllOwnerItems(@RequestHeader(RequestHttpHeaders.USER_ID) Integer userId) { + return itemClient.getAllOwnerItems(userId); + } + + @GetMapping("/search") + public ResponseEntity searchItems(@RequestParam String text) { + return text.isBlank() ? ResponseEntity.ok(List.of()) : itemClient.searchItems(text); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/item/dto/CommentSaveDto.java b/gateway/src/main/java/ru/practicum/shareit/item/dto/CommentSaveDto.java new file mode 100644 index 00000000..bed9f124 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/dto/CommentSaveDto.java @@ -0,0 +1,12 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class CommentSaveDto { + @NotNull(message = "Комментарий не может быть пустым") + @Size(min = 1, max = 300, message = "Комментарий должен быть от 1 до 300 символов") + private String text; +} diff --git a/gateway/src/main/java/ru/practicum/shareit/item/dto/ItemSaveDto.java b/gateway/src/main/java/ru/practicum/shareit/item/dto/ItemSaveDto.java new file mode 100644 index 00000000..e093a80a --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/dto/ItemSaveDto.java @@ -0,0 +1,28 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.api.ValidateCreateRequest; +import ru.practicum.shareit.api.ValidateUpdateRequest; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ItemSaveDto { + @NotNull(message = "Название не может быть пустым.", groups = {ValidateCreateRequest.class}) + @Size(min = 1, max = 50, message = "Название не может быть длиннее 50 символов.", + groups = {ValidateCreateRequest.class, ValidateUpdateRequest.class}) + String name; + + @NotNull(message = "Описание не может быть пустым.", groups = {ValidateCreateRequest.class}) + @Size(min = 1, max = 300, message = "Описание не может быть длиннее 300 символов.", + groups = {ValidateCreateRequest.class, ValidateUpdateRequest.class}) + String description; + + @NotNull(message = "Доступность не может быть пустым.", groups = {ValidateCreateRequest.class}) + Boolean available; + + Integer requestId; +} diff --git a/gateway/src/main/java/ru/practicum/shareit/request/client/ItemRequestClient.java b/gateway/src/main/java/ru/practicum/shareit/request/client/ItemRequestClient.java new file mode 100644 index 00000000..5c1ddcbc --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/request/client/ItemRequestClient.java @@ -0,0 +1,40 @@ +package ru.practicum.shareit.request.client; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.util.DefaultUriBuilderFactory; +import ru.practicum.shareit.client.BaseClient; +import ru.practicum.shareit.request.dto.ItemRequestSaveDto; + +@Service +public class ItemRequestClient extends BaseClient { + private static final String GET_ALL_ITEMS = "/all"; + private static final String REQUESTS = "/requests"; + + @Autowired + public ItemRequestClient(@Value("${shareit-server.url}") String serverUrl, RestTemplateBuilder builder) { + super(builder.uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + REQUESTS)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build()); + } + + public ResponseEntity createItemRequest(Integer userId, ItemRequestSaveDto itemRequestSaveDto) { + return post("", userId, itemRequestSaveDto); + } + + public ResponseEntity getAllUserItemRequest(Integer userId) { + return get("", userId); + } + + public ResponseEntity getAllItemRequests() { + return get(GET_ALL_ITEMS); + } + + public ResponseEntity getItemRequest(Integer requestId) { + return get("/" + requestId); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/request/controller/ItemRequestController.java b/gateway/src/main/java/ru/practicum/shareit/request/controller/ItemRequestController.java new file mode 100644 index 00000000..f7630da5 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/request/controller/ItemRequestController.java @@ -0,0 +1,43 @@ +package ru.practicum.shareit.request.controller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.api.RequestHttpHeaders; +import ru.practicum.shareit.request.client.ItemRequestClient; +import ru.practicum.shareit.request.dto.ItemRequestSaveDto; + +@Controller +@RequestMapping("/requests") +@AllArgsConstructor +@Validated +public class ItemRequestController { + + private final ItemRequestClient itemRequestClient; + + @PostMapping + public ResponseEntity createItemRequest(@RequestHeader(RequestHttpHeaders.USER_ID) Integer userId, + @RequestBody @Valid ItemRequestSaveDto itemRequestSaveDto) { + return itemRequestClient.createItemRequest(userId, itemRequestSaveDto); + } + + @GetMapping + public ResponseEntity getAllUserItemRequest(@RequestHeader(RequestHttpHeaders.USER_ID) Integer userId) { + return itemRequestClient.getAllUserItemRequest(userId); + } + + @GetMapping("/all") + public ResponseEntity getAllItemRequests() { + return itemRequestClient.getAllItemRequests(); + } + + + @GetMapping("/{requestId}") + public ResponseEntity getItemRequest(@PathVariable @PositiveOrZero Integer requestId) { + return itemRequestClient.getItemRequest(requestId); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/request/dto/ItemRequestSaveDto.java b/gateway/src/main/java/ru/practicum/shareit/request/dto/ItemRequestSaveDto.java new file mode 100644 index 00000000..bfbb2c13 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/request/dto/ItemRequestSaveDto.java @@ -0,0 +1,13 @@ +package ru.practicum.shareit.request.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + + +@Data +public class ItemRequestSaveDto { + @NotBlank + @Size(min = 1, max = 300) + private String description; +} diff --git a/gateway/src/main/java/ru/practicum/shareit/user/client/UserClient.java b/gateway/src/main/java/ru/practicum/shareit/user/client/UserClient.java new file mode 100644 index 00000000..61aa9ecb --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/user/client/UserClient.java @@ -0,0 +1,41 @@ +package ru.practicum.shareit.user.client; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.util.DefaultUriBuilderFactory; +import ru.practicum.shareit.client.BaseClient; +import ru.practicum.shareit.user.dto.UserSaveDto; + +@Service +public class UserClient extends BaseClient { + @Autowired + public UserClient(@Value("${shareit-server.url}") String serverUrl, RestTemplateBuilder builder) { + super(builder.uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + "/users")) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build()); + } + + public ResponseEntity createUser(UserSaveDto userSaveDto) { + return post("", userSaveDto); + } + + public ResponseEntity getUser(Integer userId) { + return get(getPath(userId)); + } + + public ResponseEntity updateUser(Integer userId, UserSaveDto userSaveDto) { + return patch(getPath(userId), userSaveDto); + } + + public void deleteUser(Integer userId) { + delete(getPath(userId)); + } + + private String getPath(Integer userId) { + return "/" + userId.toString(); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/user/controller/UserController.java b/gateway/src/main/java/ru/practicum/shareit/user/controller/UserController.java new file mode 100644 index 00000000..7c09b9b5 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/user/controller/UserController.java @@ -0,0 +1,45 @@ +package ru.practicum.shareit.user.controller; + +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.api.ValidateCreateRequest; +import ru.practicum.shareit.api.ValidateUpdateRequest; +import ru.practicum.shareit.user.client.UserClient; +import ru.practicum.shareit.user.dto.UserSaveDto; + +@Controller +@RequestMapping("/users") +@AllArgsConstructor +@Validated +public class UserController { + + private final UserClient userClient; + + @PostMapping + public ResponseEntity createUser(@RequestBody @Validated(ValidateCreateRequest.class) + UserSaveDto userSaveDto) { + return userClient.createUser(userSaveDto); + } + + @GetMapping("/{userId}") + public ResponseEntity getUser(@PathVariable Integer userId) { + return userClient.getUser(userId); + } + + @PatchMapping("/{userId}") + public ResponseEntity updateUser(@PathVariable Integer userId, + @RequestBody @Validated(ValidateUpdateRequest.class) + UserSaveDto userSaveDto) { + return userClient.updateUser(userId, userSaveDto); + } + + @DeleteMapping("/{userId}") + public ResponseEntity deleteUser(@PathVariable Integer userId) { + userClient.deleteUser(userId); + return new ResponseEntity(HttpStatus.OK); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/user/dto/UserSaveDto.java b/gateway/src/main/java/ru/practicum/shareit/user/dto/UserSaveDto.java new file mode 100644 index 00000000..d06d8b56 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/user/dto/UserSaveDto.java @@ -0,0 +1,22 @@ +package ru.practicum.shareit.user.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import ru.practicum.shareit.api.ValidateCreateRequest; +import ru.practicum.shareit.api.ValidateUpdateRequest; + +@Data +public class UserSaveDto { + @NotNull(message = "Необходимо указать имя", groups = {ValidateCreateRequest.class}) + @Size(min = 1, max = 50, message = "Имя должно быть от 1 до 50 символов.", + groups = {ValidateCreateRequest.class, ValidateUpdateRequest.class}) + private String name; + + @NotNull(message = "Email не может быть пустым", groups = {ValidateCreateRequest.class}) + @Email(message = "Некорректный email", groups = {ValidateCreateRequest.class, ValidateUpdateRequest.class}) + @Size(min = 1, max = 150, message = "Email должно быть от 1 до 150 символов.", + groups = {ValidateCreateRequest.class, ValidateUpdateRequest.class}) + private String email; +} diff --git a/gateway/src/main/java/ru/practicum/shareit/validation/DateTimeStartBeforeEnd.java b/gateway/src/main/java/ru/practicum/shareit/validation/DateTimeStartBeforeEnd.java new file mode 100644 index 00000000..4553bae2 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/validation/DateTimeStartBeforeEnd.java @@ -0,0 +1,18 @@ +package ru.practicum.shareit.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Target(ElementType.TYPE_USE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Constraint(validatedBy = DateTimeStartBeforeEndValidator.class) +public @interface DateTimeStartBeforeEnd { + String message() default "{\"message\": \"Дата начала не может быть раньше даты окончания\"}"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/gateway/src/main/java/ru/practicum/shareit/validation/DateTimeStartBeforeEndValidator.java b/gateway/src/main/java/ru/practicum/shareit/validation/DateTimeStartBeforeEndValidator.java new file mode 100644 index 00000000..598a0ec9 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/validation/DateTimeStartBeforeEndValidator.java @@ -0,0 +1,22 @@ +package ru.practicum.shareit.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import ru.practicum.shareit.booking.dto.BookingSaveDto; + +import java.time.LocalDateTime; + +public class DateTimeStartBeforeEndValidator implements ConstraintValidator { + + @Override + public void initialize(DateTimeStartBeforeEnd constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(BookingSaveDto bookingSaveDto, ConstraintValidatorContext constraintValidatorContext) { + LocalDateTime start = bookingSaveDto.getStart(); + LocalDateTime end = bookingSaveDto.getEnd(); + return start != null && end != null && !start.isAfter(end); + } +} diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties index 2ee08515..9df4b1d0 100644 --- a/gateway/src/main/resources/application.properties +++ b/gateway/src/main/resources/application.properties @@ -1,7 +1,5 @@ logging.level.org.springframework.web.client.RestTemplate=DEBUG #logging.level.org.apache.http=DEBUG #logging.level.httpclient.wire=DEBUG - server.port=8080 - shareit-server.url=http://localhost:9090 \ No newline at end of file diff --git a/server/pom.xml b/server/pom.xml index 566db3ea..6802fac5 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -13,51 +13,69 @@ ShareIt Server + + 21 + 1.6.2 + 0.2.0 + + + org.projectlombok + lombok-mapstruct-binding + ${lombok.mapstruct.binding.version} + + + org.mapstruct + mapstruct + ${org.mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + org.springframework.boot spring-boot-starter-data-jpa - org.springframework.boot spring-boot-starter-web - org.springframework.boot spring-boot-starter-actuator - org.postgresql postgresql runtime - com.h2database h2 runtime + 2.1.210 - org.springframework.boot spring-boot-configuration-processor true - org.projectlombok lombok true - org.springframework.boot spring-boot-starter-test test + + jakarta.validation + jakarta.validation-api + diff --git a/server/src/main/java/ru/practicum/shareit/ShareItServer.java b/server/src/main/java/ru/practicum/shareit/ShareItServer.java index 303541d8..cfabeedd 100644 --- a/server/src/main/java/ru/practicum/shareit/ShareItServer.java +++ b/server/src/main/java/ru/practicum/shareit/ShareItServer.java @@ -5,9 +5,8 @@ @SpringBootApplication public class ShareItServer { - - public static void main(String[] args) { - SpringApplication.run(ShareItServer.class, args); - } + public static void main(String[] args) { + SpringApplication.run(ShareItServer.class, args); + } } diff --git a/server/src/main/java/ru/practicum/shareit/api/RequestHttpHeaders.java b/server/src/main/java/ru/practicum/shareit/api/RequestHttpHeaders.java new file mode 100644 index 00000000..f5684d5b --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/api/RequestHttpHeaders.java @@ -0,0 +1,8 @@ +package ru.practicum.shareit.api; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class RequestHttpHeaders { + public static final String USER_ID = "X-Sharer-User-Id"; +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/api/ValidateCreateRequest.java b/server/src/main/java/ru/practicum/shareit/api/ValidateCreateRequest.java new file mode 100644 index 00000000..c3431165 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/api/ValidateCreateRequest.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.api; + +public interface ValidateCreateRequest { +} diff --git a/server/src/main/java/ru/practicum/shareit/api/ValidateUpdateRequest.java b/server/src/main/java/ru/practicum/shareit/api/ValidateUpdateRequest.java new file mode 100644 index 00000000..10d8d0ee --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/api/ValidateUpdateRequest.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.api; + +public interface ValidateUpdateRequest { +} diff --git a/server/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java b/server/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java new file mode 100644 index 00000000..e4be097b --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java @@ -0,0 +1,48 @@ +package ru.practicum.shareit.booking.controller; + +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.api.RequestHttpHeaders; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingSaveDto; +import ru.practicum.shareit.booking.model.BookingState; +import ru.practicum.shareit.booking.service.BookingService; + +import java.util.Collection; + +@RestController +@RequestMapping(path = "/bookings") +@AllArgsConstructor +public class BookingController { + private final BookingService bookingService; + + @PostMapping + BookingDto addBooking(@RequestHeader(value = RequestHttpHeaders.USER_ID) int userId, + @RequestBody BookingSaveDto bookingSaveDto) { + return bookingService.addBooking(userId, bookingSaveDto); + } + + @PatchMapping("/{bookingId}") + BookingDto manageBooking(@RequestHeader(value = RequestHttpHeaders.USER_ID) int userId, + @PathVariable int bookingId, @RequestParam boolean approved) { + return bookingService.manageBooking(userId, bookingId, approved); + } + + @GetMapping("/{bookingId}") + BookingDto getBooking(@RequestHeader(value = RequestHttpHeaders.USER_ID) int userId, + @PathVariable int bookingId) { + return bookingService.getBooking(userId, bookingId); + } + + @GetMapping + Collection getAllUserBookings(@RequestHeader(value = RequestHttpHeaders.USER_ID) int userId, + @RequestParam(defaultValue = "ALL") BookingState state) { + return bookingService.getAllUserBookings(userId, state); + } + + @GetMapping("/owner") + Collection getAllUserItemsBookings(@RequestHeader(value = RequestHttpHeaders.USER_ID) int userId, + @RequestParam(defaultValue = "ALL") BookingState state) { + return bookingService.getAllUserItemsBookings(userId, 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 new file mode 100644 index 00000000..bbdcae59 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java @@ -0,0 +1,21 @@ +package ru.practicum.shareit.booking.dto; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; +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; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class BookingDto { + int id; + LocalDateTime start; + LocalDateTime end; + BookingStatus status; + ItemDto item; + UserDto booker; +} diff --git a/server/src/main/java/ru/practicum/shareit/booking/dto/BookingSaveDto.java b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingSaveDto.java new file mode 100644 index 00000000..4d5a576a --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingSaveDto.java @@ -0,0 +1,15 @@ +package ru.practicum.shareit.booking.dto; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDateTime; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class BookingSaveDto { + Integer itemId; + LocalDateTime start; + LocalDateTime end; +} diff --git a/server/src/main/java/ru/practicum/shareit/booking/model/Booking.java b/server/src/main/java/ru/practicum/shareit/booking/model/Booking.java new file mode 100644 index 00000000..0e03b27f --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -0,0 +1,44 @@ +package ru.practicum.shareit.booking.model; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "bookings") +@FieldDefaults(level = AccessLevel.PRIVATE) +@Getter +@Setter +@ToString +public class Booking { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + int id; + + @Column(name = "start_date") + LocalDateTime start; + + @Column(name = "end_date") + LocalDateTime end; + + @ToString.Exclude + @ManyToOne + @JoinColumn(name = "item_id") + Item item; + + @ToString.Exclude + @ManyToOne + @JoinColumn(name = "booker_id") + User booker; + + @Enumerated(value = EnumType.STRING) + @Column(length = 10) + BookingStatus status; +} diff --git a/server/src/main/java/ru/practicum/shareit/booking/model/BookingState.java b/server/src/main/java/ru/practicum/shareit/booking/model/BookingState.java new file mode 100644 index 00000000..9d2e17bf --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/model/BookingState.java @@ -0,0 +1,21 @@ +package ru.practicum.shareit.booking.model; + +import java.util.Optional; + +public enum BookingState { + WAITING, + REJECTED, + CURRENT, + PAST, + FUTURE, + ALL; + + public static Optional from(String stringState) { + for (BookingState state : values()) { + if (state.name().equalsIgnoreCase(stringState)) { + return Optional.of(state); + } + } + return Optional.empty(); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java b/server/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java new file mode 100644 index 00000000..6fa13768 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/model/BookingStatus.java @@ -0,0 +1,9 @@ +package ru.practicum.shareit.booking.model; + +public enum BookingStatus { + + WAITING, + APPROVED, + REJECTED, + CANCELED +} diff --git a/server/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java b/server/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java new file mode 100644 index 00000000..e3d04238 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java @@ -0,0 +1,47 @@ +package ru.practicum.shareit.booking.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingStatus; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Optional; + +public interface BookingRepository extends JpaRepository { + + Collection findAllByItemIdAndStartAfterOrderByStartAsc(int itemId, LocalDateTime time); + + Collection findAllByItemIdInAndStartAfterOrderByStartAsc(Collection itemIds, LocalDateTime time); + + Collection findAllByBookerIdOrderByStartDesc(int bookerId); + + Collection findAllByBookerIdAndEndBeforeOrderByStartDesc(int bookerId, LocalDateTime time); + + Optional findByBookerIdAndItemIdAndEndBeforeOrderByStartDesc(int bookerId, int itemId, LocalDateTime time); + + Collection findAllByBookerIdAndStartAfterOrderByStartDesc(int bookerId, LocalDateTime time); + + @Query("SELECT b from Booking AS b " + + "WHERE b.booker.id = ?1 " + + "AND ?2 > b.start " + + "AND ?2 < b.end ") + Collection findAllCurrentBookings(int bookerId, LocalDateTime time); + + Collection findAllByBookerIdAndStatusOrderByStartDesc(int bookerId, BookingStatus status); + + Collection findAllByItemIdInAndStatusOrderByStartDesc(Collection itemIds, BookingStatus status); + + Collection findAllByItemIdInOrderByStartDesc(Collection itemIds); + + Collection findAllByItemIdInAndStartAfterOrderByStartDesc(Collection itemIds, LocalDateTime time); + + Collection findAllByItemIdInAndEndBeforeOrderByStartDesc(Collection itemIds, LocalDateTime time); + + @Query("SELECT b from Booking AS b " + + "WHERE b.item.id in ?1 " + + "AND ?2 > b.start " + + "AND ?2 < b.end ") + Collection findAllCurrentBookings(Collection itemIds, LocalDateTime time); +} diff --git a/server/src/main/java/ru/practicum/shareit/booking/service/BookingMapper.java b/server/src/main/java/ru/practicum/shareit/booking/service/BookingMapper.java new file mode 100644 index 00000000..3a7f9468 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/service/BookingMapper.java @@ -0,0 +1,20 @@ +package ru.practicum.shareit.booking.service; + +import org.mapstruct.Mapper; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingSaveDto; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.item.service.ItemMapper; +import ru.practicum.shareit.user.service.UserMapper; + +import java.util.Collection; + +@Mapper(componentModel = "spring", uses = {ItemMapper.class, UserMapper.class}) +public interface BookingMapper { + + Booking map(BookingSaveDto booking); + + BookingDto map(Booking booking); + + Collection map(Collection bookings); +} diff --git a/server/src/main/java/ru/practicum/shareit/booking/service/BookingService.java b/server/src/main/java/ru/practicum/shareit/booking/service/BookingService.java new file mode 100644 index 00000000..e221545b --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/service/BookingService.java @@ -0,0 +1,20 @@ +package ru.practicum.shareit.booking.service; + +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingSaveDto; +import ru.practicum.shareit.booking.model.BookingState; + +import java.util.Collection; + +public interface BookingService { + + BookingDto addBooking(int userId, BookingSaveDto bookingSaveDto); + + BookingDto manageBooking(int userId, int bookingId, boolean approved); + + BookingDto getBooking(int userId, int bookingId); + + Collection getAllUserBookings(int userId, BookingState state); + + Collection getAllUserItemsBookings(int userId, BookingState state); +} diff --git a/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java b/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java new file mode 100644 index 00000000..75f362f6 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java @@ -0,0 +1,167 @@ +package ru.practicum.shareit.booking.service; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingSaveDto; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingState; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.exceptions.ForbiddenException; +import ru.practicum.shareit.exceptions.ItemNotFoundException; +import ru.practicum.shareit.exceptions.NotValidException; +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.Collection; + +@Slf4j +@Service +@AllArgsConstructor +public class BookingServiceImpl implements BookingService { + private final BookingRepository bookingRepository; + private final UserRepository userRepository; + private final ItemRepository itemRepository; + private final BookingMapper bookingMapper; + + @Override + public BookingDto addBooking(int userId, BookingSaveDto bookingSaveDto) { + log.info("Запрос на добавление бронирования userId - {}, bookingSaveDto - {}", userId, bookingSaveDto); + User user = getUserById(userId); + int itemId = bookingSaveDto.getItemId(); + Item item = getItemById(itemId); + if (!item.isAvailable()) { + throw new NotValidException(Item.class, "Не доступно для бронирования"); + } + Booking booking = bookingMapper.map(bookingSaveDto); + if (booking.getStart().isAfter(booking.getEnd())) { + throw new NotValidException(Booking.class, + "Дата начала бронирования не может быть позже конца бронирования."); + } + booking.setBooker(user); + booking.setItem(item); + booking.setStatus(BookingStatus.WAITING); + Booking savedBooking = bookingRepository.save(booking); + log.info("Сохраненное создание бронирования - {}", savedBooking); + + return bookingMapper.map(savedBooking); + } + + @Override + public BookingDto manageBooking(int userId, int bookingId, boolean approved) { + log.info("Запрос на изменение бронирования по id - {}, Пользователем userId - {} и статусом - {}", bookingId, userId, approved); + getUserById(userId); + Booking booking = getBookingById(bookingId); + log.info("Бронирование для изменения - {}", booking); + if (booking.getItem().getOwner().getId() != userId) { + throw new NotValidException(Booking.class, "Только владелец может изменять бронирование"); + } + booking.setStatus(approved ? BookingStatus.APPROVED : BookingStatus.REJECTED); + Booking savedBooking = bookingRepository.save(booking); + log.info("Сохранение бронирования - {}", savedBooking); + return bookingMapper.map(savedBooking); + } + + @Override + public BookingDto getBooking(int userId, int bookingId) { + log.info("Запрос на получение бронирования по id - {}", bookingId); + getUserById(userId); + Booking booking = getBookingById(bookingId); + log.info("Бронирование найдено - {}", booking); + if (booking.getItem().getOwner().getId() != userId && booking.getBooker().getId() != userId) { + throw new NotValidException(Booking.class, + "Только владелец или человек оформивший бронирование может получить бронирование"); + } + + return bookingMapper.map(booking); + } + + @Override + public Collection getAllUserBookings(int userId, BookingState state) { + log.info("Запрос на получение всех бронирований по id - {}", userId); + getUserById(userId); + final Collection bookings; + final LocalDateTime current = LocalDateTime.now(); + switch (state) { + case WAITING -> { + bookings = bookingRepository.findAllByBookerIdAndStatusOrderByStartDesc(userId, BookingStatus.WAITING); + } + case REJECTED -> { + bookings = bookingRepository.findAllByBookerIdAndStatusOrderByStartDesc(userId, BookingStatus.REJECTED); + } + case CURRENT -> { + bookings = bookingRepository.findAllCurrentBookings(userId, current); + } + case PAST -> { + bookings = bookingRepository.findAllByBookerIdAndEndBeforeOrderByStartDesc(userId, current); + } + case FUTURE -> { + bookings = bookingRepository.findAllByBookerIdAndStartAfterOrderByStartDesc(userId, current); + } + case ALL -> { + bookings = bookingRepository.findAllByBookerIdOrderByStartDesc(userId); + } + default -> { + throw new NotValidException(BookingState.class, "invalid"); + } + } + log.info("Все бронирования найдены - {}", bookings); + return bookingMapper.map(bookings); + } + + @Override + public Collection getAllUserItemsBookings(int userId, BookingState state) { + log.info("Запрос на получение всех бронирований по userId - {}", userId); + getUserById(userId); + Collection itemIds = itemRepository.findAllByOwnerId(userId).stream() + .map(Item::getId) + .toList(); + final Collection bookings; + final LocalDateTime current = LocalDateTime.now(); + switch (state) { + case WAITING -> { + bookings = bookingRepository.findAllByItemIdInAndStatusOrderByStartDesc(itemIds, BookingStatus.WAITING); + } + case REJECTED -> { + bookings = bookingRepository.findAllByItemIdInAndStatusOrderByStartDesc(itemIds, BookingStatus.REJECTED); + } + case CURRENT -> { + bookings = bookingRepository.findAllCurrentBookings(itemIds, current); + } + case PAST -> { + bookings = bookingRepository.findAllByItemIdInAndEndBeforeOrderByStartDesc(itemIds, current); + } + case FUTURE -> { + bookings = bookingRepository.findAllByItemIdInAndStartAfterOrderByStartDesc(itemIds, current); + } + case ALL -> { + bookings = bookingRepository.findAllByItemIdInOrderByStartDesc(itemIds); + } + default -> { + throw new NotValidException(BookingState.class, "invalid"); + } + } + log.info("Все бронирования userId - {}, найдены - {}", userId, bookings); + return bookingMapper.map(bookings); + } + + private User getUserById(int userId) { + return userRepository.findById(userId).orElseThrow( + () -> new ForbiddenException("Пользователь с ID " + userId + " не найден")); + } + + private Item getItemById(int itemId) { + return itemRepository.findById(itemId).orElseThrow( + () -> new ItemNotFoundException("Предмет с ID " + itemId + " не найден")); + } + + private Booking getBookingById(int bookingId) { + return bookingRepository.findById(bookingId) + .orElseThrow(() -> new ItemNotFoundException("Бронирование с ID " + bookingId + " не найдено")); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/exceptions/DublicatedDataException.java b/server/src/main/java/ru/practicum/shareit/exceptions/DublicatedDataException.java new file mode 100644 index 00000000..20df9e2a --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exceptions/DublicatedDataException.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.exceptions; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j(topic = "DublicatedDataException") +public class DublicatedDataException extends RuntimeException { + public DublicatedDataException(String message) { + super(message); + log.error(message); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/exceptions/ErrorResponseMessage.java b/server/src/main/java/ru/practicum/shareit/exceptions/ErrorResponseMessage.java new file mode 100644 index 00000000..cc0ddb74 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exceptions/ErrorResponseMessage.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.exceptions; + +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@RequiredArgsConstructor +public class ErrorResponseMessage { + private final String error; + private final String message; +} diff --git a/server/src/main/java/ru/practicum/shareit/exceptions/ExceptionResolver.java b/server/src/main/java/ru/practicum/shareit/exceptions/ExceptionResolver.java new file mode 100644 index 00000000..de62a800 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exceptions/ExceptionResolver.java @@ -0,0 +1,86 @@ +package ru.practicum.shareit.exceptions; + +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.apache.coyote.BadRequestException; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingRequestHeaderException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.stream.Collectors; + +@Slf4j(topic = "ExceptionResolver") +@RestControllerAdvice +@ControllerAdvice +public class ExceptionResolver { + + @ExceptionHandler(UserNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorResponseMessage handleUserNotFoundException(UserNotFoundException e) { + log.error(e.getMessage()); + return new ErrorResponseMessage("User not found", e.getMessage()); + } + + @ExceptionHandler(ItemNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorResponseMessage handleItemNotFoundException(ItemNotFoundException e) { + log.error(e.getMessage()); + return new ErrorResponseMessage("Item not found", e.getMessage()); + } + + @ExceptionHandler(DublicatedDataException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorResponseMessage handleDublicatedUserException(DublicatedDataException e) { + log.error(e.getMessage()); + return new ErrorResponseMessage("Dublicated user", e.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(value = HttpStatus.BAD_REQUEST) + public ErrorResponseMessage handleValidationException(MethodArgumentNotValidException e) { + String message = e.getBindingResult() + .getFieldErrors() + .stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining(",")); + return new ErrorResponseMessage("Validation Error", message); + } + + @ExceptionHandler(BadRequestException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponseMessage handleBadRequestException(BadRequestException e) { + log.error(e.getMessage()); + return new ErrorResponseMessage("Произошла ошибка входных данных", e.getMessage()); + } + + @ExceptionHandler(MissingRequestHeaderException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponseMessage handleMissingRequestHeaderException(MissingRequestHeaderException e) { + log.error(e.getMessage()); + return new ErrorResponseMessage("Не передан Header X-Sharer-User-Id", e.getMessage()); + } + + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorResponseMessage handleConstraintViolationException(ConstraintViolationException e) { + return new ErrorResponseMessage("Constraint violation", e.getMessage()); + } + + @ExceptionHandler(ForbiddenException.class) + @ResponseStatus(value = HttpStatus.FORBIDDEN) + public ErrorResponseMessage handleForbiddenException(ForbiddenException e) { + return new ErrorResponseMessage("Доступ запрещен", e.getMessage()); + } + + @ExceptionHandler(NotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponseMessage notValidExceptionHandler(NotValidException e) { + log.error("not valid exception - {} ({})", e.getMessage(), e.getStackTrace()[0].toString()); + return new ErrorResponseMessage("BadRequest", e.getMessage()); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/exceptions/ForbiddenException.java b/server/src/main/java/ru/practicum/shareit/exceptions/ForbiddenException.java new file mode 100644 index 00000000..74f4fc8a --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exceptions/ForbiddenException.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.exceptions; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j(topic = "ForbiddenException") +public class ForbiddenException extends RuntimeException { + public ForbiddenException(String message) { + super(message); + log.error(message); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/exceptions/ItemNotFoundException.java b/server/src/main/java/ru/practicum/shareit/exceptions/ItemNotFoundException.java new file mode 100644 index 00000000..12a1591c --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exceptions/ItemNotFoundException.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.exceptions; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j(topic = "ItemNotFoundException") +public class ItemNotFoundException extends RuntimeException { + public ItemNotFoundException(String message) { + super(message); + log.error(message); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/exceptions/NotValidException.java b/server/src/main/java/ru/practicum/shareit/exceptions/NotValidException.java new file mode 100644 index 00000000..0b39fb5b --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exceptions/NotValidException.java @@ -0,0 +1,8 @@ +package ru.practicum.shareit.exceptions; + +public class NotValidException extends RuntimeException { + + public NotValidException(Class entity, String reason) { + super(entity.getSimpleName() + " " + reason); + } +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/exceptions/UserNotFoundException.java b/server/src/main/java/ru/practicum/shareit/exceptions/UserNotFoundException.java new file mode 100644 index 00000000..47322d2a --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exceptions/UserNotFoundException.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.exceptions; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j(topic = "UserNotFoundException") +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(String message) { + super(message); + log.error(message); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/item/controller/ItemController.java b/server/src/main/java/ru/practicum/shareit/item/controller/ItemController.java new file mode 100644 index 00000000..dd6492b2 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/controller/ItemController.java @@ -0,0 +1,54 @@ +package ru.practicum.shareit.item.controller; + +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.api.RequestHttpHeaders; +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.dto.CommentSaveDto; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemSaveDto; +import ru.practicum.shareit.item.service.ItemService; + +import java.util.Collection; + +@RestController +@RequestMapping("/items") +@AllArgsConstructor +public class ItemController { + private final ItemService itemService; + + @PostMapping + ItemDto addItem(@RequestHeader(value = RequestHttpHeaders.USER_ID) int userId, + @RequestBody ItemSaveDto itemSaveDto) { + return itemService.addItem(userId, itemSaveDto); + } + + @PostMapping("/{itemId}/comment") + CommentDto addComment(@RequestHeader(value = RequestHttpHeaders.USER_ID) int userId, + @PathVariable int itemId, + @RequestBody CommentSaveDto commentSaveDto) { + return itemService.addComment(userId, itemId, commentSaveDto); + } + + @PatchMapping("/{itemId}") + ItemDto updateItem(@RequestHeader(value = RequestHttpHeaders.USER_ID) int userId, + @PathVariable int itemId, + @RequestBody ItemSaveDto itemSaveDto) { + return itemService.updateItem(userId, itemId, itemSaveDto); + } + + @GetMapping("/{itemId}") + ItemDto getItem(@PathVariable int itemId) { + return itemService.getItem(itemId); + } + + @GetMapping + Collection getAllOwnerItems(@RequestHeader(value = RequestHttpHeaders.USER_ID) int userId) { + return itemService.getAllOwnerItems(userId); + } + + @GetMapping("/search") + Collection searchItems(@RequestParam String text) { + return itemService.searchItems(text); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java new file mode 100644 index 00000000..48f72773 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java @@ -0,0 +1,13 @@ +package ru.practicum.shareit.item.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class CommentDto { + private int id; + private String text; + private String authorName; + private LocalDateTime created; +} diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/CommentSaveDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/CommentSaveDto.java new file mode 100644 index 00000000..bed9f124 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/dto/CommentSaveDto.java @@ -0,0 +1,12 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class CommentSaveDto { + @NotNull(message = "Комментарий не может быть пустым") + @Size(min = 1, max = 300, message = "Комментарий должен быть от 1 до 300 символов") + private String text; +} diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java new file mode 100644 index 00000000..7d194c23 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -0,0 +1,17 @@ +package ru.practicum.shareit.item.dto; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Collection; + +@Data +public class ItemDto { + private int id; + private String name; + private String description; + private boolean available; + private LocalDateTime nextBooking; + private LocalDateTime lastBooking; + private Collection comments; +} diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/ItemSaveDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemSaveDto.java new file mode 100644 index 00000000..e093a80a --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/dto/ItemSaveDto.java @@ -0,0 +1,28 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.api.ValidateCreateRequest; +import ru.practicum.shareit.api.ValidateUpdateRequest; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ItemSaveDto { + @NotNull(message = "Название не может быть пустым.", groups = {ValidateCreateRequest.class}) + @Size(min = 1, max = 50, message = "Название не может быть длиннее 50 символов.", + groups = {ValidateCreateRequest.class, ValidateUpdateRequest.class}) + String name; + + @NotNull(message = "Описание не может быть пустым.", groups = {ValidateCreateRequest.class}) + @Size(min = 1, max = 300, message = "Описание не может быть длиннее 300 символов.", + groups = {ValidateCreateRequest.class, ValidateUpdateRequest.class}) + String description; + + @NotNull(message = "Доступность не может быть пустым.", groups = {ValidateCreateRequest.class}) + Boolean available; + + Integer requestId; +} diff --git a/server/src/main/java/ru/practicum/shareit/item/model/Comment.java b/server/src/main/java/ru/practicum/shareit/item/model/Comment.java new file mode 100644 index 00000000..8db2d2d0 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/model/Comment.java @@ -0,0 +1,37 @@ +package ru.practicum.shareit.item.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.annotations.CreationTimestamp; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "comments") +@Getter +@Setter +@ToString +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column(length = 300) + private String text; + + @ToString.Exclude + @ManyToOne + @JoinColumn(name = "item_id") + private Item item; + + @ToString.Exclude + @ManyToOne + @JoinColumn(name = "author_id") + private User author; + + @CreationTimestamp + private LocalDateTime created; +} diff --git a/server/src/main/java/ru/practicum/shareit/item/model/Item.java b/server/src/main/java/ru/practicum/shareit/item/model/Item.java new file mode 100644 index 00000000..fc909c79 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -0,0 +1,36 @@ +package ru.practicum.shareit.item.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.model.User; + +@Entity +@Table(name = "items") +@Getter +@Setter +@ToString +public class Item { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column(length = 50) + private String name; + + @Column(length = 300) + private String description; + + @Column(name = "is_available") + private boolean available; + + @ManyToOne + @JoinColumn(name = "owner_id") + private User owner; + + @ManyToOne + @JoinColumn(name = "request_id") + ItemRequest request; +} diff --git a/server/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java b/server/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java new file mode 100644 index 00000000..5ccfdd1b --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java @@ -0,0 +1,13 @@ +package ru.practicum.shareit.item.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.shareit.item.model.Comment; + +import java.util.Collection; + +public interface CommentRepository extends JpaRepository { + + Collection findAllByItemId(int itemId); + + Collection findAllByItemIdIn(Collection itemIds); +} diff --git a/server/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java b/server/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java new file mode 100644 index 00000000..a085b21d --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java @@ -0,0 +1,20 @@ +package ru.practicum.shareit.item.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import ru.practicum.shareit.item.model.Item; + +import java.util.Collection; + +public interface ItemRepository extends JpaRepository { + + Collection findAllByOwnerId(int userId); + + @Query("SELECT i FROM Item as i " + + "WHERE i.available IS TRUE " + + " AND (UPPER(i.name) LIKE UPPER(CONCAT('%',?1,'%')) " + + " OR UPPER(i.description) LIKE UPPER(CONCAT('%',?1,'%')))") + Collection searchItems(String text); + + Collection findAllByRequestId(int requestId); +} diff --git a/server/src/main/java/ru/practicum/shareit/item/service/CommentMapper.java b/server/src/main/java/ru/practicum/shareit/item/service/CommentMapper.java new file mode 100644 index 00000000..1a499bbc --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/service/CommentMapper.java @@ -0,0 +1,21 @@ +package ru.practicum.shareit.item.service; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.dto.CommentSaveDto; +import ru.practicum.shareit.item.model.Comment; +import ru.practicum.shareit.user.service.UserMapper; + +import java.util.Collection; + +@Mapper(componentModel = "spring", uses = {ItemMapper.class, UserMapper.class}) +public interface CommentMapper { + + Comment map(CommentSaveDto commentSaveDto); + + Collection map(Collection comment); + + @Mapping(source = "author", target = "authorName", qualifiedByName = "userToName") + CommentDto map(Comment comment); +} diff --git a/server/src/main/java/ru/practicum/shareit/item/service/ItemMapper.java b/server/src/main/java/ru/practicum/shareit/item/service/ItemMapper.java new file mode 100644 index 00000000..868c58b9 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/service/ItemMapper.java @@ -0,0 +1,24 @@ +package ru.practicum.shareit.item.service; + +import org.mapstruct.*; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemSaveDto; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.request.dto.ItemResponseToRequestDto; + +import java.util.Collection; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface ItemMapper { + + Item map(ItemSaveDto itemSaveDto); + + ItemDto map(Item item); + + Collection map(Collection items); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + void updateItemFromDto(@MappingTarget Item item, ItemSaveDto itemDto); + + Collection mapToResponseToRequest(Collection items); +} diff --git a/server/src/main/java/ru/practicum/shareit/item/service/ItemService.java b/server/src/main/java/ru/practicum/shareit/item/service/ItemService.java new file mode 100644 index 00000000..808d6e41 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/service/ItemService.java @@ -0,0 +1,23 @@ +package ru.practicum.shareit.item.service; + +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.dto.CommentSaveDto; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemSaveDto; + +import java.util.Collection; + +public interface ItemService { + + ItemDto addItem(int userId, ItemSaveDto itemSaveDto); + + CommentDto addComment(int userId, int itemId, CommentSaveDto commentSaveDto); + + ItemDto updateItem(int userId, int itemId, ItemSaveDto itemSaveDto); + + ItemDto getItem(int itemId); + + Collection getAllOwnerItems(int userId); + + Collection searchItems(String text); +} diff --git a/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java b/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java new file mode 100644 index 00000000..5e7f9fff --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java @@ -0,0 +1,182 @@ +package ru.practicum.shareit.item.service; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.exceptions.ForbiddenException; +import ru.practicum.shareit.exceptions.ItemNotFoundException; +import ru.practicum.shareit.exceptions.NotValidException; +import ru.practicum.shareit.exceptions.UserNotFoundException; +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.dto.CommentSaveDto; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemSaveDto; +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.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.function.Function; +import java.util.stream.Collectors; + +@Service +@AllArgsConstructor +@Slf4j(topic = "ItemServiceImpl") +public class ItemServiceImpl implements ItemService { + private final UserRepository userRepo; + private final ItemRepository itemRepo; + private final CommentRepository commentRepo; + private final BookingRepository bookingRepo; + private final ItemRequestRepository itemRequestRepo; + + private final ItemMapper itemMapper; + private final CommentMapper commentMapper; + + @Transactional + @Override + public ItemDto addItem(int userId, ItemSaveDto itemSaveDto) { + log.info("Добавление предмета пользователем {}, Предмет - {}", userId, itemSaveDto); + User owner = getUserById(userId); + Item item = itemMapper.map(itemSaveDto); + item.setOwner(owner); + Integer requestId = itemSaveDto.getRequestId(); + if (requestId != null) { + ItemRequest itemRequest = itemRequestRepo.findById(requestId) + .orElseThrow(() -> new ItemNotFoundException("Запрос с id " + requestId + " не найден.")); + item.setRequest(itemRequest); + } + Item savedItem = itemRepo.save(item); + log.info("Добавили предмет пользователем {}, Предмет - {}", userId, savedItem); + return itemMapper.map(savedItem); + } + + @Override + public CommentDto addComment(int userId, int itemId, CommentSaveDto commentSaveDto) { + User owner = getUserById(userId); + Item item = getItemById(itemId); + Booking booking = bookingRepo + .findByBookerIdAndItemIdAndEndBeforeOrderByStartDesc(userId, itemId, LocalDateTime.now()) + .orElseThrow(() -> new NotValidException(Comment.class, "cannot be posted. Please check user id " + + userId + " has previously booked item id " + itemId)); + + Comment comment = commentMapper.map(commentSaveDto); + comment.setAuthor(booking.getBooker()); + comment.setItem(booking.getItem()); + Comment savedComment = commentRepo.save(comment); + + return commentMapper.map(savedComment); + } + + @Transactional + @Override + public ItemDto updateItem(int userId, int itemId, ItemSaveDto itemSaveDto) { + Item item = getItemById(itemId); + if (item.getOwner().getId() != userId) { + throw new ForbiddenException("item could be updated only by owner"); + } + itemMapper.updateItemFromDto(item, itemSaveDto); + Item savedItem = itemRepo.save(item); + + return itemMapper.map(savedItem); + } + + @Override + public ItemDto getItem(int itemId) { + Item item = getItemById(itemId); + ItemDto itemDto = itemMapper.map(item); + Collection futureBookings = + bookingRepo.findAllByItemIdAndStartAfterOrderByStartAsc(itemId, LocalDateTime.now()).stream() + .map((Booking::getStart)) + .toList(); + + LocalDateTime nextBooking = findNextBooking(futureBookings); + itemDto.setNextBooking(nextBooking); + LocalDateTime lastBooking = findLastBooking(futureBookings); + itemDto.setLastBooking(lastBooking); + Collection commentsDto = commentMapper.map(commentRepo.findAllByItemId(itemId)); + itemDto.setComments(commentsDto); + + return itemDto; + } + + @Override + public Collection getAllOwnerItems(int userId) { + Map itemIdToItemDto = itemRepo + .findAllByOwnerId(userId) + .stream() + .map(itemMapper::map) + .collect(Collectors.toMap(ItemDto::getId, Function.identity())); + + Map> itemIdToFutureBookings = bookingRepo + .findAllByItemIdInAndStartAfterOrderByStartAsc(itemIdToItemDto.keySet(), LocalDateTime.now()) + .stream() + .collect(Collectors.groupingBy(booking -> booking.getItem().getId(), Collectors.toList())); + + Map> itemIdToComments = commentRepo + .findAllByItemIdIn(itemIdToItemDto.keySet()).stream() + .collect(Collectors.groupingBy(comment -> comment.getItem().getId(), Collectors.toList())); + + for (ItemDto itemDto : itemIdToItemDto.values()) { + if (!itemIdToFutureBookings.containsKey(itemDto.getId())) { + continue; + } + Collection futureItemBookings = itemIdToFutureBookings.get(itemDto.getId()).stream() + .map(Booking::getStart) + .toList(); + LocalDateTime nextBooking = findNextBooking(futureItemBookings); + itemDto.setNextBooking(nextBooking); + LocalDateTime lastBooking = findLastBooking(futureItemBookings); + itemDto.setLastBooking(lastBooking); + + if (!itemIdToComments.containsKey(itemDto.getId())) { + continue; + } + List comments = itemIdToComments.get(itemDto.getId()); + itemDto.setComments(commentMapper.map(comments)); + } + + return itemIdToItemDto.values(); + } + + @Override + public Collection searchItems(String text) { + if (text.isBlank()) { + return List.of(); + } + Collection items = itemRepo.searchItems(text); + return itemMapper.map(items); + } + + private LocalDateTime findNextBooking(Collection futureBookings) { + return futureBookings.stream() + .reduce((first, last) -> first) + .orElse(null); + } + + private LocalDateTime findLastBooking(Collection futureBookings) { + return futureBookings.stream() + .reduce((first, last) -> last) + .orElse(null); + } + + private User getUserById(int userId) { + return userRepo.findById(userId).orElseThrow( + () -> new UserNotFoundException("Пользователь с ID " + userId + " не найден")); + } + + private Item getItemById(int itemId) { + return itemRepo.findById(itemId).orElseThrow( + () -> new ItemNotFoundException("Предмет с id " + itemId + " не найден")); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/request/controller/ItemRequestController.java b/server/src/main/java/ru/practicum/shareit/request/controller/ItemRequestController.java new file mode 100644 index 00000000..283d452a --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/controller/ItemRequestController.java @@ -0,0 +1,39 @@ +package ru.practicum.shareit.request.controller; + +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.api.RequestHttpHeaders; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestSaveDto; +import ru.practicum.shareit.request.service.ItemRequestService; + +import java.util.Collection; + +@RestController +@RequestMapping(path = "/requests") +@AllArgsConstructor +public class ItemRequestController { + + private final ItemRequestService itemRequestService; + + @PostMapping + public ItemRequestDto createItemRequest(@RequestHeader(RequestHttpHeaders.USER_ID) Integer userId, + @RequestBody ItemRequestSaveDto itemRequestSaveDto) { + return itemRequestService.createItemRequest(userId, itemRequestSaveDto); + } + + @GetMapping + public Collection getAllUserItemRequest(@RequestHeader(RequestHttpHeaders.USER_ID) Integer userId) { + return itemRequestService.getAllUserItemRequest(userId); + } + + @GetMapping("/all") + public Collection getAllItemRequests() { + return itemRequestService.getAllItemRequests(); + } + + @GetMapping("/{requestId}") + public ItemRequestDto getItemRequest(@PathVariable int requestId) { + return itemRequestService.getItemRequest(requestId); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java new file mode 100644 index 00000000..b67b781a --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java @@ -0,0 +1,20 @@ +package ru.practicum.shareit.request.dto; + +import lombok.Data; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.Collection; + +@Data +public class ItemRequestDto { + private int id; + + private String description; + + private User requester; + + private LocalDateTime created; + + private Collection items; +} diff --git a/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestSaveDto.java b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestSaveDto.java new file mode 100644 index 00000000..bdecba1e --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestSaveDto.java @@ -0,0 +1,12 @@ +package ru.practicum.shareit.request.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class ItemRequestSaveDto { + @NotBlank + @Size(min = 1, max = 300) + private String description; +} diff --git a/server/src/main/java/ru/practicum/shareit/request/dto/ItemResponseToRequestDto.java b/server/src/main/java/ru/practicum/shareit/request/dto/ItemResponseToRequestDto.java new file mode 100644 index 00000000..abf7d781 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/dto/ItemResponseToRequestDto.java @@ -0,0 +1,10 @@ +package ru.practicum.shareit.request.dto; + +import lombok.Data; + +@Data +public class ItemResponseToRequestDto { + private int id; + private int ownerId; + private String name; +} diff --git a/server/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java b/server/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java new file mode 100644 index 00000000..cd761d0a --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java @@ -0,0 +1,32 @@ +package ru.practicum.shareit.request.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.annotations.CreationTimestamp; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "requests") +@Getter +@Setter +@ToString +public class ItemRequest { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(length = 300) + private String description; + + @ToString.Exclude + @ManyToOne + @JoinColumn(name = "requestor_id") + private User requester; + + @CreationTimestamp + private LocalDateTime created = LocalDateTime.now(); +} diff --git a/server/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java b/server/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java new file mode 100644 index 00000000..c678fdb8 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.request.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.shareit.request.model.ItemRequest; + +import java.util.Collection; + +public interface ItemRequestRepository extends JpaRepository { + + Collection findAllByRequesterIdOrderByCreatedDesc(int userId); +} diff --git a/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestMapper.java b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestMapper.java new file mode 100644 index 00000000..61efe428 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestMapper.java @@ -0,0 +1,18 @@ +package ru.practicum.shareit.request.service; + +import org.mapstruct.Mapper; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestSaveDto; +import ru.practicum.shareit.request.model.ItemRequest; + +import java.util.Collection; + +@Mapper(componentModel = "spring") +public interface ItemRequestMapper { + + ItemRequest map(ItemRequestSaveDto itemRequestSaveDto); + + ItemRequestDto map(ItemRequest itemRequest); + + Collection map(Collection itemRequests); +} diff --git a/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java new file mode 100644 index 00000000..59a2b1fe --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java @@ -0,0 +1,16 @@ +package ru.practicum.shareit.request.service; + +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestSaveDto; + +import java.util.Collection; + +public interface ItemRequestService { + ItemRequestDto createItemRequest(int userId, ItemRequestSaveDto itemRequestSaveDto); + + Collection getAllUserItemRequest(int userId); + + Collection getAllItemRequests(); + + ItemRequestDto getItemRequest(int requestId); +} diff --git a/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java new file mode 100644 index 00000000..18841376 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java @@ -0,0 +1,70 @@ +package ru.practicum.shareit.request.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.exceptions.ItemNotFoundException; +import ru.practicum.shareit.exceptions.UserNotFoundException; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.item.service.ItemMapper; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestSaveDto; +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.util.Collection; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ItemRequestServiceImpl implements ItemRequestService { + + private final UserRepository userRepo; + private final ItemRepository itemRepo; + private final ItemMapper itemMapper; + private final ItemRequestRepository itemRequestRepo; + private final ItemRequestMapper itemRequestMapper; + + + @Override + public ItemRequestDto createItemRequest(int userId, ItemRequestSaveDto itemRequestSaveDto) { + log.info("Создание заявки на предмет пользователем с id - {}, {}", userId, itemRequestSaveDto); + User user = userRepo.findById(userId).orElseThrow( + () -> new UserNotFoundException("Пользователь с id " + userId + " не найден.")); + ItemRequest itemRequest = itemRequestMapper.map(itemRequestSaveDto); + itemRequest.setRequester(user); + ItemRequest savedItemRequest = itemRequestRepo.save(itemRequest); + log.info("Создана заявка на предмет пользователем с id - {}, {}", userId, savedItemRequest); + return itemRequestMapper.map(savedItemRequest); + } + + @Override + public Collection getAllUserItemRequest(int userId) { + log.info("Получение всех заявок пользователя с id - {}", userId); + return itemRequestMapper.map(itemRequestRepo.findAllByRequesterIdOrderByCreatedDesc(userId)); + } + + @Override + public Collection getAllItemRequests() { + log.info("Получение всех заявок"); + return itemRequestMapper.map(itemRequestRepo.findAll(Sort.by(Sort.Direction.DESC, "created"))); + } + + @Override + public ItemRequestDto getItemRequest(int requestId) { + log.info("Получение заявки с id - {}", requestId); + ItemRequest itemRequest = itemRequestRepo.findById(requestId) + .orElseThrow( + () -> new ItemNotFoundException("Заявка с id " + requestId + " не найдена.")); + Collection items = itemRepo.findAllByRequestId(requestId); + items.forEach(item -> log.debug("item id - {}, owner id - {}", item.getId(), item.getOwner().getId())); + ItemRequestDto itemRequestDto = itemRequestMapper.map(itemRequest); + itemRequestDto.setItems(itemMapper.mapToResponseToRequest(items)); + log.info("Найдена заявка {}", itemRequestDto); + return itemRequestDto; + } +} diff --git a/server/src/main/java/ru/practicum/shareit/user/controller/UserController.java b/server/src/main/java/ru/practicum/shareit/user/controller/UserController.java new file mode 100644 index 00000000..8ed71483 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/controller/UserController.java @@ -0,0 +1,38 @@ +package ru.practicum.shareit.user.controller; + +import lombok.AllArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.api.ValidateCreateRequest; +import ru.practicum.shareit.api.ValidateUpdateRequest; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.dto.UserSaveDto; +import ru.practicum.shareit.user.service.UserService; + +@RestController +@RequestMapping(path = "/users") +@AllArgsConstructor +public class UserController { + private final UserService userService; + + @PostMapping + UserDto createUser(@RequestBody @Validated(ValidateCreateRequest.class) UserSaveDto userSaveDto) { + return userService.createUser(userSaveDto); + } + + @GetMapping("/{userId}") + UserDto getUser(@PathVariable Integer userId) { + return userService.getUser(userId); + } + + @PatchMapping("/{userId}") + UserDto updateUser(@PathVariable Integer userId, + @RequestBody @Validated(ValidateUpdateRequest.class) UserSaveDto userSaveDto) { + return userService.updateUser(userId, userSaveDto); + } + + @DeleteMapping("/{userId}") + void deleteUser(@PathVariable Integer userId) { + userService.deleteUser(userId); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/user/dto/UserDto.java b/server/src/main/java/ru/practicum/shareit/user/dto/UserDto.java new file mode 100644 index 00000000..a157d64a --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/dto/UserDto.java @@ -0,0 +1,10 @@ +package ru.practicum.shareit.user.dto; + +import lombok.Data; + +@Data +public class UserDto { + private int id; + private String name; + private String email; +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/user/dto/UserSaveDto.java b/server/src/main/java/ru/practicum/shareit/user/dto/UserSaveDto.java new file mode 100644 index 00000000..d06d8b56 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/dto/UserSaveDto.java @@ -0,0 +1,22 @@ +package ru.practicum.shareit.user.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import ru.practicum.shareit.api.ValidateCreateRequest; +import ru.practicum.shareit.api.ValidateUpdateRequest; + +@Data +public class UserSaveDto { + @NotNull(message = "Необходимо указать имя", groups = {ValidateCreateRequest.class}) + @Size(min = 1, max = 50, message = "Имя должно быть от 1 до 50 символов.", + groups = {ValidateCreateRequest.class, ValidateUpdateRequest.class}) + private String name; + + @NotNull(message = "Email не может быть пустым", groups = {ValidateCreateRequest.class}) + @Email(message = "Некорректный email", groups = {ValidateCreateRequest.class, ValidateUpdateRequest.class}) + @Size(min = 1, max = 150, message = "Email должно быть от 1 до 150 символов.", + groups = {ValidateCreateRequest.class, ValidateUpdateRequest.class}) + private String email; +} diff --git a/server/src/main/java/ru/practicum/shareit/user/model/User.java b/server/src/main/java/ru/practicum/shareit/user/model/User.java new file mode 100644 index 00000000..50652720 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/model/User.java @@ -0,0 +1,23 @@ +package ru.practicum.shareit.user.model; + +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Entity +@Table(name = "users") +@Getter +@Setter +@ToString +@EqualsAndHashCode(of = "id") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + @Column(length = 50) + private String name; + @Column(length = 150) + private String email; +} diff --git a/server/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java b/server/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java new file mode 100644 index 00000000..f211fe72 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java @@ -0,0 +1,8 @@ +package ru.practicum.shareit.user.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.shareit.user.model.User; + +public interface UserRepository extends JpaRepository { +} + diff --git a/server/src/main/java/ru/practicum/shareit/user/service/UserMapper.java b/server/src/main/java/ru/practicum/shareit/user/service/UserMapper.java new file mode 100644 index 00000000..45ebe897 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/service/UserMapper.java @@ -0,0 +1,22 @@ +package ru.practicum.shareit.user.service; + +import org.mapstruct.*; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.dto.UserSaveDto; +import ru.practicum.shareit.user.model.User; + +@Mapper(componentModel = "spring") +public interface UserMapper { + + @Named("userToName") + static String userToName(User user) { + return user.getName(); + } + + User map(UserSaveDto userSaveDto); + + UserDto map(User user); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + void updateUserFromDto(@MappingTarget User user, UserSaveDto userDto); +} diff --git a/server/src/main/java/ru/practicum/shareit/user/service/UserService.java b/server/src/main/java/ru/practicum/shareit/user/service/UserService.java new file mode 100644 index 00000000..de2568cd --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/service/UserService.java @@ -0,0 +1,15 @@ +package ru.practicum.shareit.user.service; + +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.dto.UserSaveDto; + +public interface UserService { + + UserDto createUser(UserSaveDto userSaveDto); + + UserDto getUser(Integer userId); + + UserDto updateUser(Integer userId, UserSaveDto userSaveDto); + + void deleteUser(Integer userId); +} diff --git a/server/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java b/server/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java new file mode 100644 index 00000000..b580b363 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java @@ -0,0 +1,44 @@ +package ru.practicum.shareit.user.service; + +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.exceptions.UserNotFoundException; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.dto.UserSaveDto; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +@Service +@AllArgsConstructor +public class UserServiceImpl implements UserService { + private final UserRepository userRepository; + private final UserMapper userMapper; + + @Override + public UserDto createUser(UserSaveDto userSaveDto) { + User user = userMapper.map(userSaveDto); + User savedUser = userRepository.save(user); + return userMapper.map(savedUser); + } + + @Override + public UserDto getUser(Integer userId) { + User user = userRepository.findById(userId).orElseThrow( + () -> new UserNotFoundException("Пользователь с ID " + userId + " не найден")); + return userMapper.map(user); + } + + @Override + public UserDto updateUser(Integer userId, UserSaveDto userSaveDto) { + User user = userRepository.findById(userId).orElseThrow( + () -> new UserNotFoundException("Пользователь с ID " + userId + " не найден")); + userMapper.updateUserFromDto(user, userSaveDto); + User savedUser = userRepository.save(user); + return userMapper.map(savedUser); + } + + @Override + public void deleteUser(Integer userId) { + userRepository.deleteById(userId); + } +} diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 6a96840f..9384dd3f 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -1,17 +1,41 @@ server.port=9090 - -spring.jpa.hibernate.ddl-auto=none -spring.jpa.properties.hibernate.format_sql=true -spring.sql.init.mode=always - #--- spring.datasource.driverClassName=org.postgresql.Driver -spring.datasource.url=jdbc:postgresql://localhost:5432/shareit +#spring.datasource.url=jdbc:postgresql://localhost:5432/shareit +spring.datasource.url=jdbc:h2:file:./db/shareit spring.datasource.username=shareit spring.datasource.password=shareit #--- spring.config.activate.on-profile=test spring.datasource.driverClassName=org.h2.Driver -spring.datasource.url=jdbc:h2:mem:shareit +spring.datasource.url=jdbc:h2:file:./db/shareit spring.datasource.username=shareit -spring.datasource.password=shareit \ No newline at end of file +spring.datasource.password=shareit +#JPA +spring.jpa.hibernate.ddl-auto=none +spring.jpa.properties.hibernate.format_sql=true +spring.sql.init.mode=always +hibernate.show_sql=true +hibernate.jdbc.time_zone=UTC +#Logging +logging.level.org.springframework.orm.jpa=TRACE +logging.level.org.springframework.web=TRACE +logging.level.org.springframework.transaction=TRACE +logging.level.org.springframework.transaction.interceptor=TRACE +logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.transaction.interceptor=trace +logging.level.org.hibernate.orm.jdbc.bind=trace +logging.level.org.hibernate.orm.jpa.JpaTransactionManager=debug +jdbc.driverClassName=org.postgresql.Driver +jdbc.url=jdbc:postgresql://localhost:6541/shareit +jdbc.username=shareit +jdbc.password=shareit +#spring.datasource.url=jdbc:h2:file:./db/shareit +#spring.datasource.driverClassName=org.h2.Driver +#spring.datasource.username=sa +#spring.datasource.password=password +#spring.h2.console.enabled=true +server.servlet.encoding.charset=UTF-8 +server.servlet.encoding.force-response=true + diff --git a/server/src/main/resources/schema.sql b/server/src/main/resources/schema.sql new file mode 100644 index 00000000..8826da89 --- /dev/null +++ b/server/src/main/resources/schema.sql @@ -0,0 +1,45 @@ +drop table IF EXISTS users, + items, + requests, + items, + bookings, + comments; + +create TABLE IF NOT EXISTS users ( + id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name varchar(50) NOT NULL, + email varchar(50) NOT NULL UNIQUE +); + +create TABLE IF NOT EXISTS requests ( + id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + description varchar(300) NOT NULL, + requestor_id int NOT NULL REFERENCES users(id) ON delete CASCADE, + created timestamp without time zone +); + +create TABLE IF NOT EXISTS items ( + id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name varchar(50) NOT NULL, + description varchar(300) NOT NULL, + is_available boolean NOT NULL, + owner_id int NOT NULL REFERENCES users(id) ON delete CASCADE, + request_id int REFERENCES requests(id) +); + +create TABLE IF NOT EXISTS bookings ( + id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + start_date timestamp without time zone, + end_date timestamp without time zone, + item_id int NOT NULL REFERENCES items(id) ON delete CASCADE, + booker_id int NOT NULL REFERENCES users(id) ON delete CASCADE, + status varchar(10) NOT NULL +); + +create TABLE IF NOT EXISTS comments ( + id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + text varchar(300) NOT NULL, + item_id int NOT NULL REFERENCES items(id) ON delete CASCADE, + author_id int NOT NULL REFERENCES users(id) ON delete CASCADE, + created timestamp without time zone +); \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/ShareItServerTests.java b/server/src/test/java/ru/practicum/shareit/ShareItServerTests.java new file mode 100644 index 00000000..f33d350e --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/ShareItServerTests.java @@ -0,0 +1,15 @@ +package ru.practicum.shareit; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("test") +class ShareItServerTests { + + @Test + void contextLoads() { + } + +} diff --git a/server/src/test/java/ru/practicum/shareit/booking/controller/BookingControllerTest.java b/server/src/test/java/ru/practicum/shareit/booking/controller/BookingControllerTest.java new file mode 100644 index 00000000..cfcf9a54 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/controller/BookingControllerTest.java @@ -0,0 +1,180 @@ +package ru.practicum.shareit.booking.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jdk.jfr.ContentType; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +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.api.RequestHttpHeaders; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingSaveDto; +import ru.practicum.shareit.booking.model.BookingState; +import ru.practicum.shareit.booking.model.BookingStatus; +import ru.practicum.shareit.booking.service.BookingService; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.user.dto.UserDto; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static org.mockito.internal.verification.VerificationModeFactory.times; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(BookingController.class) +@AutoConfigureMockMvc +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class BookingControllerTest { + private final MockMvc mockMvc; + private final ObjectMapper objectMapper; + + @MockBean + private final BookingService service; + private BookingDto bookingExpected; + + @BeforeEach + public void testInit() { + LocalDateTime start = LocalDateTime.now(); + LocalDateTime end = start.plusMinutes(1); + + ItemDto item = new ItemDto(); + item.setId(1); + item.setName("laptop"); + item.setDescription("Apple laptop for working on java ShareIt project"); + item.setAvailable(true); + + UserDto booker = new UserDto(); + booker.setId(1); + booker.setName("Booker"); + booker.setEmail("booker@yandex.ru"); + + bookingExpected = new BookingDto(); + bookingExpected.setId(1); + bookingExpected.setStart(start); + bookingExpected.setEnd(end); + bookingExpected.setStatus(BookingStatus.WAITING); + bookingExpected.setItem(item); + bookingExpected.setBooker(booker); + + + } + + @Test + void addBookingTest() throws Exception { + int userId = 1; + BookingSaveDto bookingSaveDto = new BookingSaveDto(); + bookingSaveDto.setItemId(1); + bookingSaveDto.setStart(LocalDateTime.now()); + bookingSaveDto.setEnd(LocalDateTime.now().plusMinutes(1)); + String bookingSaveDtoJson = objectMapper.writeValueAsString(bookingSaveDto); + String bookingExpectedJson = objectMapper.writeValueAsString(bookingExpected); + + when(service.addBooking(eq(userId), any(BookingSaveDto.class))) + .thenReturn(bookingExpected); + mockMvc.perform(post("/bookings") + .header(RequestHttpHeaders.USER_ID, String.valueOf(userId)) + .contentType(MediaType.APPLICATION_JSON) + .content(bookingSaveDtoJson)) + .andExpect(status().isOk()) + .andExpect(content().json(bookingExpectedJson)); + + verify(service, times(1)).addBooking(eq(userId), any(BookingSaveDto.class)); + } + + @Test + void manageBookingTest() throws Exception { + int userId = 1; + int bookingId = 1; + boolean approved = true; + String path = "/bookings/" + bookingId; + + when(service.manageBooking(eq(userId), eq(bookingId), eq(approved))) + .thenAnswer(invocationOnMock -> { + bookingExpected.setStatus(BookingStatus.APPROVED); + return bookingExpected; + }); + + mockMvc.perform(patch(path) + .header(RequestHttpHeaders.USER_ID, String.valueOf(userId)) + .param("approved", String.valueOf(approved)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(BookingStatus.APPROVED.name())); + + verify(service, times(1)).manageBooking(eq(userId), eq(bookingId), eq(approved)); + } + + @Test + void getBookingTest() throws Exception { + int userId = 1; + int bookingId = 1; + String path = "/bookings/" + bookingId; + String bookingExpectedJson = objectMapper.writeValueAsString(bookingExpected); + + when(service.getBooking(eq(userId), eq(bookingId))) + .thenReturn(bookingExpected); + + mockMvc.perform(get(path) + .header(RequestHttpHeaders.USER_ID, String.valueOf(userId)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(bookingExpected))); + + verify(service, times(1)).getBooking(eq(userId), eq(bookingId)); + } + + @Test + void getAllUserBookingsTest() throws Exception { + int userId = 1; + BookingState state = BookingState.REJECTED; + + when(service.getAllUserBookings(eq(userId), eq(state))) + .thenReturn(Collections.emptyList()); + + mockMvc.perform(get("/bookings") + .header(RequestHttpHeaders.USER_ID, String.valueOf(userId)) + .contentType(MediaType.APPLICATION_JSON) + .param("state", String.valueOf(state)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json("[]")); + + verify(service, times(1)).getAllUserBookings(eq(userId), eq(state)); + } + + @Test + void getAllUserItemsBookingsTest() throws Exception { + int userId = 1; + BookingState state = BookingState.WAITING; + String path = "/bookings" + "/owner"; + List expectedBookings = List.of(bookingExpected); + String expectedBookingsJson = objectMapper.writeValueAsString(expectedBookings); + + when(service.getAllUserItemsBookings(eq(userId), eq(state))) + .thenReturn(expectedBookings); + mockMvc.perform(get(path) + .header(RequestHttpHeaders.USER_ID, userId) + .param("state", String.valueOf(state)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()", is(1))) + .andExpect(content().json(expectedBookingsJson)); + + verify(service, times(1)).getAllUserItemsBookings(eq(userId), eq(state)); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/booking/service/BookingServiceImplTest.java b/server/src/test/java/ru/practicum/shareit/booking/service/BookingServiceImplTest.java new file mode 100644 index 00000000..f5917eef --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/service/BookingServiceImplTest.java @@ -0,0 +1,203 @@ +package ru.practicum.shareit.booking.service; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingSaveDto; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingState; +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.Collection; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +@Transactional +@SpringBootTest +@RequiredArgsConstructor(onConstructor = @__(@Autowired)) +class BookingServiceImplTest { + private final EntityManager entityManager; + private final BookingService bookingService; + private final BookingMapper mapper; + + private Booking bookingExpected; + private Item itemExpected; + private User userExpected; + + @BeforeEach + public void setUp() { + + userExpected = new User(); + userExpected.setId(30); + userExpected.setName("user3"); + userExpected.setEmail("user3@somemail.ru"); + + itemExpected = new Item(); + itemExpected.setId(30); + itemExpected.setName("item3"); + itemExpected.setDescription("description3"); + itemExpected.setAvailable(true); + itemExpected.setOwner(userExpected); + + bookingExpected = new Booking(); + bookingExpected.setId(1); + bookingExpected.setItem(itemExpected); + bookingExpected.setBooker(userExpected); + bookingExpected.setStatus(BookingStatus.WAITING); + bookingExpected.setStart(LocalDateTime.of(2024, 12, 11, 11, 11, 11)); + bookingExpected.setEnd(LocalDateTime.of(2024, 12, 11, 11, 12, 11)); + + + } + + + @Test + public void addBookingTest() { + int userId = userExpected.getId(); + BookingSaveDto bookingSaveDto = new BookingSaveDto(); + bookingSaveDto.setItemId(itemExpected.getId()); + bookingSaveDto.setStart(bookingExpected.getStart()); + bookingSaveDto.setEnd(bookingExpected.getEnd()); + + bookingService.addBooking(userId, bookingSaveDto); + TypedQuery query = + entityManager.createQuery("SELECT b FROM Booking b WHERE b.id = :id", Booking.class); + Booking booking = query.setParameter("id", bookingExpected.getId()).getSingleResult(); + BookingDto bookingDto = mapper.map(booking); + + BookingDto bookingDtoExpected = mapper.map(bookingExpected); + assertThat(bookingDto, allOf( + hasProperty("id", + equalTo(bookingDtoExpected.getId())), + hasProperty("start", + equalTo(bookingDtoExpected.getStart())), + hasProperty("end", + equalTo(bookingDtoExpected.getEnd())), + hasProperty("item", + allOf(hasProperty("id", equalTo(bookingDtoExpected.getItem().getId())))), + hasProperty("booker", + allOf(hasProperty("id", equalTo(bookingDtoExpected.getBooker().getId())))), + hasProperty("status", + equalTo(bookingDtoExpected.getStatus())) + )); + } + + @Test + void manageBookingTest() { + int userId = 10; + int bookingId = 10; + boolean approved = true; + + bookingService.manageBooking(userId, bookingId, approved); + TypedQuery query = entityManager.createQuery("SELECT b FROM Booking AS b WHERE b.id = :id", Booking.class); + Booking booking = query.setParameter("id", bookingId).getSingleResult(); + BookingDto bookingDto = mapper.map(booking); + + long bookerExpectedId = 20; + assertThat(bookingDto, allOf( + hasProperty("id", equalTo(bookingId)), + hasProperty("booker", allOf(hasProperty("id", equalTo(bookerExpectedId)))), + hasProperty("status", equalTo(BookingStatus.APPROVED)) + )); + } + + @Test + void getBookingTest() { + int userId = 20; + int bookingId = 10; + + BookingDto bookingDto = bookingService.getBooking(userId, bookingId); + + BookingDto bookingId10DtoExpected = new BookingDto(); + bookingId10DtoExpected.setId(10); + LocalDateTime start = LocalDateTime.of(2024, 11, 21, 10, 10, 10); + bookingId10DtoExpected.setStart(start); + LocalDateTime end = LocalDateTime.of(2024, 11, 21, 11, 11, 11); + bookingId10DtoExpected.setEnd(end); + ItemDto item = new ItemDto(); + item.setId(10); + bookingId10DtoExpected.setItem(item); + UserDto booker = new UserDto(); + booker.setId(20); + bookingId10DtoExpected.setBooker(booker); + bookingId10DtoExpected.setStatus(BookingStatus.APPROVED); + + assertThat(bookingDto, allOf( + hasProperty("id", + equalTo(bookingId10DtoExpected.getId())), + hasProperty("start", + equalTo(bookingId10DtoExpected.getStart())), + hasProperty("end", + equalTo(bookingId10DtoExpected.getEnd())), + hasProperty("item", + allOf(hasProperty("id", equalTo(bookingId10DtoExpected.getItem().getId())))), + hasProperty("booker", + allOf(hasProperty("id", equalTo(bookingId10DtoExpected.getBooker().getId())))), + hasProperty("status", + equalTo(bookingId10DtoExpected.getStatus())) + )); + } + + @Test + void getAllUserBookingsTest() { + int userId = 20; + BookingState state = BookingState.WAITING; + + Collection bookings = bookingService.getAllUserItemsBookings(userId, state); + + bookings.forEach(booking -> assertThat(booking, allOf( + hasProperty("id", notNullValue()), + hasProperty("start", nullValue()), + hasProperty("end", nullValue()), + hasProperty("item", allOf( + hasProperty("id", notNullValue()), + hasProperty("name", notNullValue()), + hasProperty("description", notNullValue()), + hasProperty("available", notNullValue()) + )), + hasProperty("booker", allOf( + hasProperty("id", notNullValue()), + hasProperty("email", notNullValue()), + hasProperty("name", notNullValue()) + )), + hasProperty("status", notNullValue())) + )); + } + + @Test + void getAllUserItemsBookingsTest() { + int userId = 20; + BookingState state = BookingState.ALL; + + Collection bookings = bookingService.getAllUserItemsBookings(userId, state); + bookings.forEach(booking -> assertThat(booking, allOf( + hasProperty("id", notNullValue()), + hasProperty("start", nullValue()), + hasProperty("end", nullValue()), + hasProperty("item", allOf( + hasProperty("id", notNullValue()), + hasProperty("name", notNullValue()), + hasProperty("description", notNullValue()), + hasProperty("available", notNullValue()) + )), + hasProperty("booker", allOf( + hasProperty("id", notNullValue()), + hasProperty("email", notNullValue()), + hasProperty("name", notNullValue()) + )), + hasProperty("status", notNullValue())) + )); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/controller/ItemControllerTest.java b/server/src/test/java/ru/practicum/shareit/item/controller/ItemControllerTest.java new file mode 100644 index 00000000..d6106dc7 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/controller/ItemControllerTest.java @@ -0,0 +1,190 @@ +package ru.practicum.shareit.item.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +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.api.RequestHttpHeaders; +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.dto.CommentSaveDto; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemSaveDto; +import ru.practicum.shareit.item.service.ItemService; + +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.mockito.internal.verification.VerificationModeFactory.times; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + + +@WebMvcTest(ItemController.class) +@AutoConfigureMockMvc +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class ItemControllerTest { + + private final MockMvc mockMvc; + private final ObjectMapper objectMapper; + + @MockBean + private final ItemService itemService; + private ItemDto expectedItem; + private CommentDto expectedComment; + + @BeforeEach + public void initTest() { + expectedItem = new ItemDto(); + expectedItem.setId(1); + expectedItem.setName("item"); + expectedItem.setDescription("description"); + expectedItem.setAvailable(false); + + expectedComment = new CommentDto(); + expectedComment.setId(1); + expectedComment.setText("comment"); + expectedComment.setAuthorName("user1"); + } + + @Test + void addItemTest() throws Exception { + int userId = 1; + ItemSaveDto itemSaveDto = new ItemSaveDto(); + itemSaveDto.setName("item"); + itemSaveDto.setDescription("description"); + itemSaveDto.setAvailable(false); + String itemSaveDtoJson = objectMapper.writeValueAsString(itemSaveDto); + String itemDtoExpectedJson = objectMapper.writeValueAsString(expectedItem); + + when(itemService.addItem(any(Integer.class), any(ItemSaveDto.class))) + .thenReturn(expectedItem); + + mockMvc.perform(post("/items") + .header(RequestHttpHeaders.USER_ID, String.valueOf(userId)) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(itemSaveDtoJson)) + .andExpect(status().isOk()) + .andExpect(content().json(itemDtoExpectedJson)); + + verify(itemService, times(1)).addItem(any(Integer.class), any(ItemSaveDto.class)); + + } + + @Test + void addCommentTest() throws Exception { + int userId = 1; + int itemId = expectedItem.getId(); + String path = "/items" + "/" + itemId + "/comment"; + CommentSaveDto comment = new CommentSaveDto(); + comment.setText(expectedComment.getText()); + String commentJson = objectMapper.writeValueAsString(comment); + String commentExpectedJson = objectMapper.writeValueAsString(expectedComment); + + when(itemService.addComment(any(Integer.class), eq(itemId), eq(comment))) + .thenReturn(expectedComment); + + mockMvc.perform(post(path) + .header(RequestHttpHeaders.USER_ID, userId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(commentJson)) + .andExpect(status().isOk()) + .andExpect(content().json(commentExpectedJson)); + + verify(itemService, times(1)).addComment(any(Integer.class), eq(itemId), eq(comment)); + } + + @Test + void updateItemTest() throws Exception { + int userId = 1; + int itemId = expectedItem.getId(); + String path = "/items" + "/" + itemId; + ItemSaveDto itemSaveDtoForUpdate = new ItemSaveDto(); + itemSaveDtoForUpdate.setDescription("updated description"); + itemSaveDtoForUpdate.setAvailable(true); + String itemSaveDtoForUpdateJson = objectMapper.writeValueAsString(itemSaveDtoForUpdate); + + when(itemService.updateItem(eq(userId), eq(itemId), any(ItemSaveDto.class))) + .thenAnswer(invocationOnMock -> { + expectedItem.setDescription(itemSaveDtoForUpdate.getDescription()); + expectedItem.setAvailable(itemSaveDtoForUpdate.getAvailable()); + return expectedItem; + }); + + mockMvc.perform(patch(path) + .header(RequestHttpHeaders.USER_ID, userId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(itemSaveDtoForUpdateJson)) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(expectedItem))); + + verify(itemService, times(1)).updateItem(eq(userId), eq(itemId), any(ItemSaveDto.class)); + } + + @Test + void getItemTest() throws Exception { + int userId = 1; + String path = "/items" + "/" + userId; + + when(itemService.getItem(eq(userId))) + .thenReturn(expectedItem); + String userDtoExpectedJson = objectMapper.writeValueAsString(expectedItem); + + mockMvc.perform(get(path) + .header(RequestHttpHeaders.USER_ID, userId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(userDtoExpectedJson)); + + verify(itemService, times(1)).getItem(eq(userId)); + } + + @Test + void getAllOwnerItemsTest() throws Exception { + int userId = 1; + List itemsExpected = List.of(expectedItem); + String itemsExpectedJson = objectMapper.writeValueAsString(itemsExpected); + + when(itemService.getAllOwnerItems(eq(userId))) + .thenReturn(itemsExpected); + + mockMvc.perform(get("/items") + .header(RequestHttpHeaders.USER_ID, userId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()", is(1))) + .andExpect(content().json(itemsExpectedJson)); + + verify(itemService, times(1)).getAllOwnerItems(eq(userId)); + } + + @Test + void searchItemsTest() throws Exception { + String searchText = expectedItem.getDescription(); + List itemsExpected = List.of(expectedItem); + String itemsExpectedJson = objectMapper.writeValueAsString(itemsExpected); + + when(itemService.searchItems(eq(searchText))) + .thenReturn(itemsExpected); + + mockMvc.perform(get("/items/search") + .param("text", searchText) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(itemsExpectedJson)); + + verify(itemService, times(1)).searchItems(eq(searchText)); + } + +} + diff --git a/server/src/test/java/ru/practicum/shareit/item/service/ItemServiceImplTest.java b/server/src/test/java/ru/practicum/shareit/item/service/ItemServiceImplTest.java new file mode 100644 index 00000000..a85bbaf6 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/service/ItemServiceImplTest.java @@ -0,0 +1,227 @@ +package ru.practicum.shareit.item.service; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import ru.practicum.shareit.exceptions.ForbiddenException; +import ru.practicum.shareit.exceptions.ItemNotFoundException; +import ru.practicum.shareit.exceptions.NotValidException; +import ru.practicum.shareit.exceptions.UserNotFoundException; +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.dto.CommentSaveDto; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemSaveDto; +import ru.practicum.shareit.item.model.Item; + +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Transactional +@SpringBootTest +@RequiredArgsConstructor(onConstructor = @__(@Autowired)) +class ItemServiceImplTest { + private final EntityManager em; + private final ItemService service; + private final ItemMapper itemMapper; + private ItemDto itemAddedExpected; + private ItemDto itemExistedExpected; + private CommentDto commentAddedExpected; + + private static final int NON_EXISTENT_ID = 999; + + @BeforeEach + public void testInit() { + itemAddedExpected = new ItemDto(); + itemAddedExpected.setId(1); + itemAddedExpected.setName("item"); + itemAddedExpected.setDescription("description"); + itemAddedExpected.setAvailable(false); + + commentAddedExpected = new CommentDto(); + commentAddedExpected.setId(1); + commentAddedExpected.setText("comment"); + + itemExistedExpected = new ItemDto(); + itemExistedExpected.setId(40); + itemExistedExpected.setName("item4"); + itemExistedExpected.setDescription("description4"); + itemExistedExpected.setAvailable(true); + CommentDto comment = new CommentDto(); + comment.setId(40); + comment.setText("comment4"); + comment.setAuthorName("user5"); + itemExistedExpected.setComments(List.of(comment)); + } + + @Test + void addItemTest() { + int userId = 10; + ItemSaveDto item = new ItemSaveDto(); + item.setName(itemAddedExpected.getName()); + item.setDescription(itemAddedExpected.getDescription()); + item.setAvailable(itemAddedExpected.isAvailable()); + long itemAddedExpectedId = itemAddedExpected.getId(); + + service.addItem(userId, item); + TypedQuery query = em.createQuery("SELECT i FROM Item AS i WHERE i.id = :itemId", Item.class); + ItemDto itemAdded = itemMapper.map(query.setParameter("itemId", itemAddedExpectedId).getSingleResult()); + + assertThat(itemAdded, allOf( + hasProperty("id", equalTo(itemAddedExpected.getId())), + hasProperty("name", equalTo(itemAddedExpected.getName())), + hasProperty("description", equalTo(itemAddedExpected.getDescription())), + hasProperty("available", equalTo(itemAddedExpected.isAvailable())) + )); + } + + @Test + void addItemNonExistentTest() { + ItemSaveDto item = new ItemSaveDto(); + item.setName(itemAddedExpected.getName()); + item.setDescription(itemAddedExpected.getDescription()); + item.setAvailable(itemAddedExpected.isAvailable()); + + assertThrows(UserNotFoundException.class, () -> service.addItem(NON_EXISTENT_ID, item)); + } + + @Test + public void addCommentTest() { + + int userId = 20; + int itemId = 50; + CommentSaveDto comment = new CommentSaveDto(); + comment.setText(commentAddedExpected.getText()); + + CommentDto commentAdded = service.addComment(userId, itemId, comment); + + assertThat(commentAdded, allOf( + hasProperty("id", equalTo(commentAddedExpected.getId())), + hasProperty("text", equalTo(commentAddedExpected.getText()) + ))); + } + + @Test + public void addCommentWithoutOwningItemTest() { + int userId = 10; + int itemId = 10; + CommentSaveDto comment = new CommentSaveDto(); + comment.setText(commentAddedExpected.getText()); + + assertThrows(NotValidException.class, () -> service.addComment(userId, itemId, comment)); + } + + @Test + public void updateItem() { + int userId = 10; + int itemId = 10; + ItemSaveDto itemForUpdate = new ItemSaveDto(); + itemForUpdate.setName("item"); + itemForUpdate.setDescription("description"); + itemForUpdate.setAvailable(false); + + service.updateItem(userId, itemId, itemForUpdate); + TypedQuery query = em.createQuery("SELECT i FROM Item AS i WHERE i.id = :itemId", Item.class); + ItemDto itemUpdated = itemMapper.map(query.setParameter("itemId", itemId).getSingleResult()); + + assertThat(itemUpdated, allOf( + hasProperty("id", equalTo(itemId)), + hasProperty("name", equalTo(itemForUpdate.getName())), + hasProperty("description", equalTo(itemForUpdate.getDescription())), + hasProperty("available", equalTo(itemForUpdate.getAvailable())) + )); + } + + @Test + public void updateItemNotExistTest() { + int userId = 10; + ItemSaveDto itemForUpdate = new ItemSaveDto(); + itemForUpdate.setName("item"); + itemForUpdate.setDescription("description"); + itemForUpdate.setAvailable(false); + + assertThrows(ItemNotFoundException.class, () -> service.updateItem(userId, NON_EXISTENT_ID, itemForUpdate)); + } + + @Test + public void updateItemNotOwnerTest() { + int userId = 20; + int itemId = 10; + ItemSaveDto itemForUpdate = new ItemSaveDto(); + itemForUpdate.setName("item"); + itemForUpdate.setDescription("description"); + itemForUpdate.setAvailable(false); + + assertThrows(ForbiddenException.class, () -> service.updateItem(userId, itemId, itemForUpdate)); + } + + @Test + void getItem() { + int itemId = itemExistedExpected.getId(); + CommentDto commentExpected = itemExistedExpected.getComments().stream() + .toList() + .getFirst(); + + ItemDto item = service.getItem(itemId); + + assertThat(item, allOf( + hasProperty("id", equalTo(itemExistedExpected.getId())), + hasProperty("name", equalTo(itemExistedExpected.getName())), + hasProperty("description", equalTo(itemExistedExpected.getDescription())), + hasProperty("available", equalTo(itemExistedExpected.isAvailable())) + )); + + List comments = item.getComments() + .stream() + .toList(); + assertEquals(1, comments.size()); + assertThat(comments.getFirst(), allOf( + hasProperty("id", equalTo(commentExpected.getId())), + hasProperty("text", equalTo(commentExpected.getText())), + hasProperty("authorName", equalTo(commentExpected.getAuthorName())) + )); + } + + @Test + void getAllOwnerItems() { + int userId = 40; + + List items = service.getAllOwnerItems(userId) + .stream() + .toList(); + + assertEquals(1, items.size()); + ItemDto item = items.getFirst(); + assertThat(item, allOf( + hasProperty("id", equalTo(itemExistedExpected.getId())), + hasProperty("name", equalTo(itemExistedExpected.getName())), + hasProperty("description", equalTo(itemExistedExpected.getDescription())), + hasProperty("available", equalTo(itemExistedExpected.isAvailable())) + )); + } + + @Test + void searchItems() { + String text = "description4"; + List items = service.searchItems(text) + .stream() + .toList(); + + assertEquals(1, items.size()); + ItemDto item = items.getFirst(); + assertThat(item, allOf( + hasProperty("id", equalTo(itemExistedExpected.getId())), + hasProperty("name", equalTo(itemExistedExpected.getName())), + hasProperty("description", equalTo(itemExistedExpected.getDescription())), + hasProperty("available", equalTo(itemExistedExpected.isAvailable())) + )); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/request/controller/ItemRequestControllerTest.java b/server/src/test/java/ru/practicum/shareit/request/controller/ItemRequestControllerTest.java new file mode 100644 index 00000000..49711989 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/controller/ItemRequestControllerTest.java @@ -0,0 +1,106 @@ +package ru.practicum.shareit.request.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +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.api.RequestHttpHeaders; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestSaveDto; +import ru.practicum.shareit.request.service.ItemRequestService; + +import java.util.Collection; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.internal.verification.VerificationModeFactory.times; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(ItemRequestController.class) +@AutoConfigureMockMvc +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class ItemRequestControllerTest { + private final MockMvc mockMvc; + private final ObjectMapper objectMapper; + @MockBean + private final ItemRequestService service; + private ItemRequestDto itemRequestExpected; + + @BeforeEach + public void testInit() { + itemRequestExpected = new ItemRequestDto(); + itemRequestExpected.setId(1); + itemRequestExpected.setDescription("description"); + } + + @Test + void createItemRequestTest() throws Exception { + int userId = 1; + ItemRequestSaveDto itemRequest = new ItemRequestSaveDto(); + itemRequest.setDescription(itemRequestExpected.getDescription()); + String itemRequestJson = objectMapper.writeValueAsString(itemRequest); + String itemRequestExpectedJson = objectMapper.writeValueAsString(itemRequestExpected); + + when(service.createItemRequest(any(Integer.class), any(ItemRequestSaveDto.class))) + .thenReturn(itemRequestExpected); + + mockMvc.perform(post("/requests") + .header(RequestHttpHeaders.USER_ID, userId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(itemRequestJson)) + .andExpect(status().isOk()) + .andExpect(content().json(itemRequestExpectedJson)); + + verify(service, times(1)) + .createItemRequest(any(Integer.class), any(ItemRequestSaveDto.class)); + } + + @Test + void getAllUserItemRequestTest() throws Exception { + int userId = 10; + Collection itemRequestsExpected = List.of(itemRequestExpected); + String itemRequestsExpectedJson = objectMapper.writeValueAsString(itemRequestsExpected); + + when(service.getAllUserItemRequest(eq(userId))) + .thenReturn(itemRequestsExpected); + + mockMvc.perform(get("/requests") + .header(RequestHttpHeaders.USER_ID, userId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(itemRequestsExpectedJson)); + + verify(service, times(1)).getAllUserItemRequest(eq(userId)); + } + + @Test + void getAllItemRequestsTest() throws Exception { + + int itemRequestId = itemRequestExpected.getId(); + String path = "/requests" + "/" + itemRequestId; + String itemRequestExpectedJson = objectMapper.writeValueAsString(itemRequestExpected); + + when(service.getItemRequest(eq(itemRequestId))) + .thenReturn(itemRequestExpected); + + mockMvc.perform(get(path) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(itemRequestExpectedJson)); + + verify(service, times(1)).getItemRequest(eq(itemRequestId)); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceImplTest.java b/server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceImplTest.java new file mode 100644 index 00000000..c2c0b634 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceImplTest.java @@ -0,0 +1,110 @@ +package ru.practicum.shareit.request.service; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.exceptions.UserNotFoundException; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.dto.ItemRequestSaveDto; +import ru.practicum.shareit.request.model.ItemRequest; + +import java.util.Collection; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +@Transactional +@SpringBootTest +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class ItemRequestServiceImplTest { + private final EntityManager em; + private final ItemRequestService service; + private final ItemRequestMapper mapper; + private ItemRequestDto itemRequestCreatedExpected; + private ItemRequestDto itemRequestExistedExpected; + + private static final int NON_EXISTENT_ID = 999; + + @BeforeEach + public void testInit() { + itemRequestCreatedExpected = new ItemRequestDto(); + itemRequestCreatedExpected.setId(1); + itemRequestCreatedExpected.setDescription("description"); + itemRequestExistedExpected = new ItemRequestDto(); + itemRequestExistedExpected.setId(10); + itemRequestExistedExpected.setDescription("description1"); + } + + @Test + void createItemRequestTest() { + int userId = 10; + ItemRequestSaveDto itemRequest = new ItemRequestSaveDto(); + itemRequest.setDescription(itemRequestCreatedExpected.getDescription()); + int itemRequestExpectedId = itemRequestCreatedExpected.getId(); + + service.createItemRequest(userId, itemRequest); + TypedQuery query = + em.createQuery("SELECT r FROM ItemRequest AS r WHERE r.id = :id", ItemRequest.class); + ItemRequestDto itemRequestCreated = mapper + .map(query.setParameter("id", itemRequestExpectedId).getSingleResult()); + + assertThat(itemRequestCreated, allOf( + hasProperty("id", equalTo(itemRequestCreatedExpected.getId())), + hasProperty("description", equalTo(itemRequestCreatedExpected.getDescription())) + )); + } + + @Test + void createItemRequestWithNonExistentUserTest() { + ItemRequestSaveDto itemRequest = new ItemRequestSaveDto(); + itemRequest.setDescription(itemRequestCreatedExpected.getDescription()); + assertThrows(UserNotFoundException.class, () -> service.createItemRequest(NON_EXISTENT_ID, itemRequest)); + } + + @Test + void getAllUserItemRequestTest() { + int userId = itemRequestExistedExpected.getId(); + + List itemRequests = service.getAllUserItemRequest(userId) + .stream() + .toList(); + assertEquals(itemRequests.size(), 1); + assertThat(itemRequests.getFirst(), allOf( + hasProperty("id", equalTo(itemRequestExistedExpected.getId())), + hasProperty("description", equalTo(itemRequestExistedExpected.getDescription())) + )); + } + + @Test + void getAllItemRequestsTest() { + int itemRequestExpectedId = 10; + Collection itemRequests = service.getAllItemRequests(); + assertEquals(itemRequests.size(), 5); + for (ItemRequestDto request : itemRequests) { + assertThat(request, allOf( + hasProperty("id", equalTo(itemRequestExpectedId)), + hasProperty("description", equalTo("description" + itemRequestExpectedId / 10)) + )); + itemRequestExpectedId += 10; + } + } + + @Test + void getItemRequestTest() { + int itemRequestId = itemRequestExistedExpected.getId(); + ItemRequestDto itemRequest = service.getItemRequest(itemRequestId); + assertThat(itemRequest, allOf( + hasProperty("id", equalTo(itemRequestExistedExpected.getId())), + hasProperty("description", equalTo(itemRequestExistedExpected.getDescription())) + )); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/controller/UserControllerTest.java b/server/src/test/java/ru/practicum/shareit/user/controller/UserControllerTest.java new file mode 100644 index 00000000..93bc8bce --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/controller/UserControllerTest.java @@ -0,0 +1,126 @@ +package ru.practicum.shareit.user.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +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.api.RequestHttpHeaders; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.dto.UserSaveDto; +import ru.practicum.shareit.user.service.UserService; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.internal.verification.VerificationModeFactory.times; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(UserController.class) +@AutoConfigureMockMvc +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class UserControllerTest { + private final MockMvc mockMvc; + private final ObjectMapper objectMapper; + @MockBean + private final UserService service; + private UserDto userExpected; + + @BeforeEach + void testInit() { + userExpected = new UserDto(); + userExpected.setId(1); + userExpected.setName("User1"); + userExpected.setEmail("user1@yandex.ru"); + } + + @Test + void createUser() throws Exception { + + int userId = userExpected.getId(); + UserSaveDto userSaveDto = new UserSaveDto(); + userSaveDto.setName(userExpected.getName()); + userSaveDto.setEmail(userExpected.getEmail()); + String userSaveDtoJson = objectMapper.writeValueAsString(userSaveDto); + + when(service.createUser(any(UserSaveDto.class))) + .thenReturn(userExpected); + String userDtoExpectedJson = objectMapper.writeValueAsString(userExpected); + + mockMvc.perform(post("/users") + .header(RequestHttpHeaders.USER_ID, userId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(userSaveDtoJson)) + .andExpect(status().isOk()) + .andExpect(content().json(userDtoExpectedJson)); + + verify(service, times(1)).createUser(any(UserSaveDto.class)); + } + + @Test + void getUserTest() throws Exception { + int userId = userExpected.getId(); + String path = "/users" + "/" + userId; + + when(service.getUser(eq(userId))) + .thenReturn(userExpected); + String userDtoExpectedJson = objectMapper.writeValueAsString(userExpected); + + mockMvc.perform(get(path) + .header(RequestHttpHeaders.USER_ID, userId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(userDtoExpectedJson)); + + verify(service, times(1)).getUser(eq(userId)); + } + + @Test + void updateUserTest() throws Exception { + int userId = userExpected.getId(); + UserSaveDto userSaveDtoForUpdate = new UserSaveDto(); + String nameUpdated = "Superuser"; + userSaveDtoForUpdate.setName(nameUpdated); + String emailUpdated = "superuser@yandex.ru"; + userSaveDtoForUpdate.setEmail(emailUpdated); + String userSaveDtoForUpdateJson = objectMapper.writeValueAsString(userSaveDtoForUpdate); + String path = "/users" + "/" + userId; + + when(service.updateUser(eq(userId), eq(userSaveDtoForUpdate))) + .thenAnswer(invocationOnMock -> { + userExpected.setName(nameUpdated); + userExpected.setEmail(emailUpdated); + return userExpected; + }); + mockMvc.perform(patch(path) + .header(RequestHttpHeaders.USER_ID, userId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(userSaveDtoForUpdateJson)) + .andExpect(status().isOk()) + .andExpect(content().json(objectMapper.writeValueAsString(userExpected))); + + verify(service, times(1)).updateUser(eq(userId), eq(userSaveDtoForUpdate)); + } + + @Test + void deleteUserTest() throws Exception { + int userId = userExpected.getId(); + String path = "/users" + "/" + userId; + mockMvc.perform(delete(path) + .header(RequestHttpHeaders.USER_ID, userId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + verify(service, times(1)).deleteUser(eq(userId)); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/service/UserServiceImplTest.java b/server/src/test/java/ru/practicum/shareit/user/service/UserServiceImplTest.java new file mode 100644 index 00000000..c6dec013 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/service/UserServiceImplTest.java @@ -0,0 +1,104 @@ +package ru.practicum.shareit.user.service; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.dto.UserSaveDto; +import ru.practicum.shareit.user.model.User; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Transactional +@SpringBootTest +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class UserServiceImplTest { + private final EntityManager em; + private final UserService service; + private final UserMapper mapper; + private UserDto userCreatedExpected; + private UserDto userExistedExpected; + + private static final long NON_EXISTENT_ID = 999; + + @BeforeEach + public void testInit() { + userCreatedExpected = new UserDto(); + userCreatedExpected.setId(1); + userCreatedExpected.setName("user"); + userCreatedExpected.setEmail("user@somemail.ru"); + userExistedExpected = new UserDto(); + userExistedExpected.setId(10); + userExistedExpected.setName("user1"); + userExistedExpected.setEmail("user1@somemail.ru"); + } + + @Test + void createUserTest() { + UserSaveDto userSaveDto = new UserSaveDto(); + userSaveDto.setName(userCreatedExpected.getName()); + userSaveDto.setEmail(userCreatedExpected.getEmail()); + int userCreatedExpectedId = userCreatedExpected.getId(); + + service.createUser(userSaveDto); + TypedQuery query = em.createQuery("SELECT u FROM User AS u where u.id = :userId", User.class); + UserDto userCreated = mapper.map(query.setParameter("userId", userCreatedExpectedId).getSingleResult()); + + assertThat(userCreated, allOf( + hasProperty("id", equalTo(userCreatedExpectedId)), + hasProperty("name", equalTo(userCreatedExpected.getName())), + hasProperty("email", equalTo(userCreatedExpected.getEmail())) + )); + } + + @Test + void getUserTest() { + int userId = userExistedExpected.getId(); + + UserDto user = service.getUser(userId); + + assertThat(user, allOf( + hasProperty("id", equalTo(userExistedExpected.getId())), + hasProperty("name", equalTo(userExistedExpected.getName())), + hasProperty("email", equalTo(userExistedExpected.getEmail())) + )); + } + + @Test + void updateUserTest() { + int userId = 10; + UserSaveDto userSaveDtoForUpdate = new UserSaveDto(); + userSaveDtoForUpdate.setName("user"); + userSaveDtoForUpdate.setEmail("user@yandex.ru"); + + service.updateUser(userId, userSaveDtoForUpdate); + TypedQuery query = em.createQuery("SELECT u FROM User AS u where u.id = :userId", User.class); + UserDto userUpdated = mapper.map(query.setParameter("userId", userId).getSingleResult()); + + assertThat(userUpdated, allOf( + hasProperty("id", equalTo(userId)), + hasProperty("name", equalTo(userSaveDtoForUpdate.getName())), + hasProperty("email", equalTo(userSaveDtoForUpdate.getEmail())) + )); + } + + @Test + void deleteUserTest() { + int userId = 60; + service.deleteUser(userId); + + TypedQuery query = em.createQuery("SELECT u FROM User AS u", User.class); + + assertTrue(query.getResultStream() + .filter(user -> user.getId() == userId) + .findFirst() + .isEmpty()); + } +} \ No newline at end of file diff --git a/server/src/test/resources/application-test.properties b/server/src/test/resources/application-test.properties new file mode 100644 index 00000000..85cca43b --- /dev/null +++ b/server/src/test/resources/application-test.properties @@ -0,0 +1,16 @@ +server.port=9090 + +spring.config.activate.on-profile=test + +spring.application.name=ShareItServerTest +spring.main.banner-mode=OFF + +spring.datasource.username=shareit +spring.datasource.password=shareit +spring.datasource.url=jdbc:h2:file:./dbtest/shareit +spring.datasource.driver-class-name=org.h2.Driver + +spring.jpa.show-sql=true +spring.jpa.hibernate.ddl-auto=none +spring.jpa.properties.hibernate.format_sql=true +spring.sql.init.mode=always \ No newline at end of file diff --git a/server/src/test/resources/data.sql b/server/src/test/resources/data.sql new file mode 100644 index 00000000..f0717a22 --- /dev/null +++ b/server/src/test/resources/data.sql @@ -0,0 +1,37 @@ +INSERT INTO users (id, name, email) +VALUES (10, 'user1', 'user1@somemail.ru'), + (20, 'user2', 'user2@somemail.ru'), + (30, 'user3', 'user3@somemail.ru'), + (40, 'user4', 'user4@somemail.ru'), + (50, 'user5', 'user5@somemail.ru'), + (60, 'user6', 'user6@somemail.ru'); + +INSERT INTO requests (id, description, requestor_id, created) +VALUES (10, 'description1', 10, null), + (20, 'description2', 20, null), + (30, 'description3', 30, null), + (40, 'description4', 40, null), + (50, 'description5', 50, null); + +INSERT INTO items (id, name, description, is_available, owner_id, request_id) +VALUES (10, 'item1', 'description1', 'true', 10, 10), + (20, 'item2', 'description2', 'true', 10, 20), + (30, 'item3', 'description3', 'true', 30, 30), + (40, 'item4', 'description4', 'true', 40, 40), + (50, 'item5', 'description5', 'false', 50, 50); + +INSERT INTO bookings (id, start_date, end_date, item_id, booker_id, status) +VALUES (10, TIMESTAMP '2024-12-11 10:10:10', TIMESTAMP '2024-12-11 11:11:11', 10, 20, 'APPROVED'), + (20, CAST(CURRENT_DATE AS TIMESTAMP) + INTERVAL '1' DAY, CAST(CURRENT_DATE AS TIMESTAMP) + INTERVAL '2' DAY, 40, 40, 'APPROVED'), + (30, null, null, 30, 20, 'WAITING'), + (40, null, null, 40, 20, 'WAITING'), + (50, TIMESTAMP '2024-12-11 10:10:10', TIMESTAMP '2024-12-11 11:11:11', 50, 20, 'APPROVED'), + (60, TIMESTAMP '2024-12-11 10:10:10', TIMESTAMP '2024-12-11 11:11:11', 10, 20, 'REJECTED'), + (70, TIMESTAMP '2024-12-11 10:10:10', TIMESTAMP '2024-12-11 11:11:11', 10, 20, 'WAITING'); + +INSERT INTO comments (id, text, item_id, author_id, created) +VALUES (10, 'comment1', 10, 20, null), + (20, 'comment1', 10, 10, null), + (30, 'comment3', 10, 30, null), + (40, 'comment4', 40, 50, null), + (50, 'comment5', 50, 40, null); \ No newline at end of file