diff --git a/pom.xml b/pom.xml index 6ddbe30..5b84756 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,16 @@ spring-boot-configuration-processor true + + com.h2database + h2 + test + + + org.springframework.boot + spring-boot-starter-test + test + org.postgresql @@ -71,6 +81,10 @@ 1.5.5.Final provided + + org.springframework.boot + spring-boot-starter-data-jpa + diff --git a/src/main/java/ru/practicum/shareit/booking/BookingMapper.java b/src/main/java/ru/practicum/shareit/booking/BookingMapper.java new file mode 100644 index 0000000..1df9f1a --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingMapper.java @@ -0,0 +1,30 @@ +package ru.practicum.shareit.booking; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingShortDto; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.item.ItemMapper; +import ru.practicum.shareit.user.UserMapper; + +@Mapper(componentModel = "spring", + uses = {ItemMapper.class, UserMapper.class}, + nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) +public interface BookingMapper { + + @Mapping(target = "item", source = "item") + @Mapping(target = "booker", source = "booker") + BookingDto toBookingDto(Booking booking); + + @Mapping(target = "id", ignore = true) + Booking toBooking(BookingDto bookingDto); + + Booking updateBookingFields(@MappingTarget Booking targetBooking, BookingDto sourceBookingDto); + + @Mapping(target = "id", source = "id") + @Mapping(target = "bookerId", source = "booker.id") + BookingShortDto toBookingShortDto(Booking booking); +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/BookingState.java b/src/main/java/ru/practicum/shareit/booking/BookingState.java new file mode 100644 index 0000000..ecb6b3a --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/BookingState.java @@ -0,0 +1,10 @@ +package ru.practicum.shareit.booking; + +public enum BookingState { + ALL, + CURRENT, + PAST, + FUTURE, + WAITING, + REJECTED +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/Impl/BookingServiceImpl.java b/src/main/java/ru/practicum/shareit/booking/Impl/BookingServiceImpl.java new file mode 100644 index 0000000..e9d7f03 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/Impl/BookingServiceImpl.java @@ -0,0 +1,152 @@ +package ru.practicum.shareit.booking.Impl; + +import org.springframework.stereotype.Service; +import ru.practicum.shareit.booking.BookingMapper; +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.BookingStatus; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.exception.ShareItException; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.booking.service.BookerStateProcessor; +import ru.practicum.shareit.booking.service.BookingService; +import ru.practicum.shareit.booking.service.handler.owner.OwnerStateProcessor; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class BookingServiceImpl implements BookingService { + private final BookingRepository bookingRepository; + private final UserRepository userRepository; + private final ItemRepository itemRepository; + private final BookingMapper bookingMapper; + private final BookerStateProcessor bookerStateProcessor; + private final OwnerStateProcessor ownerStateProcessor; + + public BookingServiceImpl(BookingRepository bookingRepository, + UserRepository userRepository, + ItemRepository itemRepository, + BookingMapper bookingMapper, + BookerStateProcessor bookerStateProcessor, + OwnerStateProcessor ownerStateProcessor) { + this.bookingRepository = bookingRepository; + this.userRepository = userRepository; + this.itemRepository = itemRepository; + this.bookingMapper = bookingMapper; + this.bookerStateProcessor = bookerStateProcessor; + this.ownerStateProcessor = ownerStateProcessor; + } + + @Override + public BookingDto create(BookingDto bookingDto, Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new ShareItException.NotFoundException("Пользователь с id " + userId + " не найден")); + + if (bookingDto.getItemId() == null) { + throw new ShareItException.BadRequestException("ID предмета не указан"); + } + + Item item = itemRepository.findById(bookingDto.getItemId()) + .orElseThrow(() -> new ShareItException.NotFoundException("Предмет с id " + bookingDto.getItemId() + " не найден")); + + if (!item.getAvailable()) { + throw new ShareItException.BadRequestException("Предмет недоступен для бронирования"); + } + + if (item.getOwner().getId().equals(userId)) { + throw new ShareItException.NotFoundException("Владелец не может бронировать свой предмет"); + } + + LocalDateTime now = LocalDateTime.now(); + if (bookingDto.getStart() != null && bookingDto.getEnd() != null) { + if (bookingDto.getEnd().isBefore(bookingDto.getStart()) || bookingDto.getEnd().equals(bookingDto.getStart())) { + throw new ShareItException.BadRequestException("Дата окончания должна быть позже даты начала"); + } + } + + Booking booking = new Booking(); + booking.setStart(bookingDto.getStart()); + booking.setEnd(bookingDto.getEnd()); + booking.setItem(item); + booking.setBooker(user); + booking.setStatus(BookingStatus.WAITING); + + Booking savedBooking = bookingRepository.save(booking); + return bookingMapper.toBookingDto(savedBooking); + } + + @Override + public BookingDto approve(Long bookingId, Long userId, Boolean approved) { + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new ShareItException.NotFoundException("Бронирование с id " + bookingId + " не найдено")); + + if (!booking.getItem().getOwner().getId().equals(userId)) { + throw new ShareItException.ForbiddenException("Только владелец предмета может подтверждать бронирование"); + } + + if (booking.getStatus() != BookingStatus.WAITING) { + throw new ShareItException.BadRequestException("Бронирование уже обработано"); + } + + booking.setStatus(approved ? BookingStatus.APPROVED : BookingStatus.REJECTED); + return bookingMapper.toBookingDto(bookingRepository.save(booking)); + } + + @Override + public BookingDto getById(Long bookingId, Long userId) { + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new ShareItException.NotFoundException("Бронирование с id " + bookingId + " не найдено")); + + if (!booking.getBooker().getId().equals(userId) && !booking.getItem().getOwner().getId().equals(userId)) { + throw new ShareItException.NotFoundException("Доступ запрещен"); + } + + return bookingMapper.toBookingDto(booking); + } + + @Override + public List getAllByBooker(Long userId, String stateParam) { + userRepository.findById(userId) + .orElseThrow(() -> new ShareItException.NotFoundException("Пользователь с id " + userId + " не найден")); + + BookingState state; + try { + state = BookingState.valueOf(stateParam.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new ShareItException.BadRequestException("Неизвестный статус: " + stateParam); + } + + LocalDateTime now = LocalDateTime.now(); + List bookings = bookerStateProcessor.process(state, userId, now); + + return bookings.stream() + .map(bookingMapper::toBookingDto) + .collect(Collectors.toList()); + } + + @Override + public List getAllByOwner(Long userId, String stateParam) { + userRepository.findById(userId) + .orElseThrow(() -> new ShareItException.NotFoundException("Пользователь с id " + userId + " не найден")); + + BookingState state; + try { + state = BookingState.valueOf(stateParam.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new ShareItException.BadRequestException("Неизвестный статус: " + stateParam); + } + + LocalDateTime now = LocalDateTime.now(); + List bookings = ownerStateProcessor.process(state, userId, now); + + return bookings.stream() + .map(bookingMapper::toBookingDto) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java b/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java index a3e0ebb..558a388 100644 --- a/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java +++ b/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java @@ -1,10 +1,58 @@ package ru.practicum.shareit.booking.controller; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +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 org.springframework.web.bind.annotation.RestController; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.service.BookingService; +import java.util.List; @RestController @RequestMapping(path = "/bookings") +@RequiredArgsConstructor public class BookingController { + private final BookingService bookingService; + + @PostMapping + public ResponseEntity createBooking(@Valid @RequestBody BookingDto bookingDto, + @RequestHeader("X-Sharer-User-Id") Long userId) { + return ResponseEntity.ok(bookingService.create(bookingDto, userId)); + } + + @PatchMapping("/{bookingId}") + public ResponseEntity approve(@PathVariable Long bookingId, + @RequestHeader("X-Sharer-User-Id") Long userId, + @RequestParam Boolean approved) { + return ResponseEntity.ok(bookingService.approve(bookingId, userId, approved)); + } + + @GetMapping("/{bookingId}") + public ResponseEntity getById(@PathVariable Long bookingId, + @RequestHeader("X-Sharer-User-Id") Long userId) { + return ResponseEntity.ok(bookingService.getById(bookingId, userId)); + } + + @GetMapping + public ResponseEntity> getAllByBooker( + @RequestHeader("X-Sharer-User-Id") Long userId, + @RequestParam(defaultValue = "ALL") String state) { + return ResponseEntity.ok(bookingService.getAllByBooker(userId, state)); + } + + @GetMapping("/owner") + public ResponseEntity> getAllByOwner( + @RequestHeader("X-Sharer-User-Id") Long userId, + @RequestParam(defaultValue = "ALL") String state) { + return ResponseEntity.ok(bookingService.getAllByOwner(userId, state)); + } } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java b/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java index 393e544..39ccfba 100644 --- a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java @@ -1,5 +1,7 @@ package ru.practicum.shareit.booking.dto; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.Getter; import lombok.Setter; @@ -9,15 +11,18 @@ import java.time.LocalDateTime; - @Getter @Setter @Builder(toBuilder = true) public class BookingDto { private Long id; + @NotNull(message = "Дата начала бронирования не может быть пустой") + @Future(message = "Дата начала бронирования должна быть в будущем") private LocalDateTime start; + @NotNull(message = "Дата окончания бронирования не может быть пустой") + @Future(message = "Дата окончания бронирования должна быть в будущем") private LocalDateTime end; private ItemDto item; @@ -25,4 +30,6 @@ public class BookingDto { private UserDto booker; private BookingStatus status; + + private Long itemId; } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingInputDto.java b/src/main/java/ru/practicum/shareit/booking/dto/BookingInputDto.java new file mode 100644 index 0000000..12cd8a0 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingInputDto.java @@ -0,0 +1,16 @@ +package ru.practicum.shareit.booking.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Builder +public class BookingInputDto { + private Long itemId; + private LocalDateTime start; + private LocalDateTime end; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingShortDto.java b/src/main/java/ru/practicum/shareit/booking/dto/BookingShortDto.java new file mode 100644 index 0000000..9066ffd --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/dto/BookingShortDto.java @@ -0,0 +1,18 @@ +package ru.practicum.shareit.booking.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Builder(toBuilder = true) +public class BookingShortDto { + private Long id; + private LocalDateTime start; + private LocalDateTime end; + private Long bookerId; + private Long itemId; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/exception/ShareItException.java b/src/main/java/ru/practicum/shareit/booking/exception/ShareItException.java index db4ad68..9fe4a12 100644 --- a/src/main/java/ru/practicum/shareit/booking/exception/ShareItException.java +++ b/src/main/java/ru/practicum/shareit/booking/exception/ShareItException.java @@ -28,4 +28,18 @@ public ConflictException(String message) { super(message, HttpStatus.CONFLICT); } } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + public static class BadRequestException extends ShareItException { + public BadRequestException(String message) { + super(message, HttpStatus.BAD_REQUEST); + } + } + + @ResponseStatus(HttpStatus.FORBIDDEN) + public static class ForbiddenException extends ShareItException { + public ForbiddenException(String message) { + super(message, HttpStatus.FORBIDDEN); + } + } } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/model/Booking.java b/src/main/java/ru/practicum/shareit/booking/model/Booking.java index 5382265..6ce1971 100644 --- a/src/main/java/ru/practicum/shareit/booking/model/Booking.java +++ b/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -1,6 +1,20 @@ package ru.practicum.shareit.booking.model; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import ru.practicum.shareit.booking.BookingStatus; import ru.practicum.shareit.item.model.Item; @@ -8,19 +22,33 @@ import java.time.LocalDateTime; - +@Entity +@Table(name = "bookings") @Getter @Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor public class Booking { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "start_date", nullable = false) private LocalDateTime start; + @Column(name = "end_date", nullable = false) private LocalDateTime end; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id", nullable = false) private Item item; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "booker_id", nullable = false) private User booker; + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) private BookingStatus status; } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java new file mode 100644 index 0000000..d613465 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java @@ -0,0 +1,73 @@ +package ru.practicum.shareit.booking.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import ru.practicum.shareit.booking.BookingStatus; +import ru.practicum.shareit.booking.model.Booking; + +import java.time.LocalDateTime; +import java.util.List; + +public interface BookingRepository extends JpaRepository { + + List findAllByBookerId(Long bookerId); + + List findAllByBookerIdAndStartBeforeAndEndAfter( + Long bookerId, LocalDateTime start, LocalDateTime end); + + List findAllByBookerIdAndEndBefore(Long bookerId, LocalDateTime end); + + List findAllByBookerIdAndStartAfter(Long bookerId, LocalDateTime start); + + List findAllByBookerIdAndStatus(Long bookerId, BookingStatus status); + + @Query("SELECT b FROM Booking b WHERE b.item.owner.id = :ownerId") + List findAllByItemOwnerId(@Param("ownerId") Long ownerId); + + @Query("SELECT b FROM Booking b WHERE b.item.owner.id = :ownerId " + + "AND b.start < :start AND b.end > :end") + List findAllByItemOwnerIdAndStartBeforeAndEndAfter( + @Param("ownerId") Long ownerId, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end); + + @Query("SELECT b FROM Booking b WHERE b.item.owner.id = :ownerId " + + "AND b.end < :end") + List findAllByItemOwnerIdAndEndBefore( + @Param("ownerId") Long ownerId, + @Param("end") LocalDateTime end); + + @Query("SELECT b FROM Booking b WHERE b.item.owner.id = :ownerId " + + "AND b.start > :start") + List findAllByItemOwnerIdAndStartAfter( + @Param("ownerId") Long ownerId, + @Param("start") LocalDateTime start); + + @Query("SELECT b FROM Booking b WHERE b.item.owner.id = :ownerId " + + "AND b.status = :status") + List findAllByItemOwnerIdAndStatus( + @Param("ownerId") Long ownerId, + @Param("status") BookingStatus status); + + @Query("SELECT b FROM Booking b WHERE b.item.id = :itemId " + + "AND b.start < :now AND b.status = 'APPROVED' " + + "ORDER BY b.start DESC") + List findLastBookingForItem( + @Param("itemId") Long itemId, + @Param("now") LocalDateTime now); + + @Query("SELECT b FROM Booking b WHERE b.item.id = :itemId " + + "AND b.start > :now AND b.status = 'APPROVED' " + + "ORDER BY b.start ASC") + List findNextBookingForItem( + @Param("itemId") Long itemId, + @Param("now") LocalDateTime now); + + @Query("SELECT COUNT(b) > 0 FROM Booking b WHERE b.booker.id = :userId " + + "AND b.item.id = :itemId AND b.end < :now AND b.status = 'APPROVED'") + boolean hasUserBookedItem( + @Param("userId") Long userId, + @Param("itemId") Long itemId, + @Param("now") LocalDateTime now); +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/service/BookerStateProcessor.java b/src/main/java/ru/practicum/shareit/booking/service/BookerStateProcessor.java new file mode 100644 index 0000000..b8281ea --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/BookerStateProcessor.java @@ -0,0 +1,27 @@ +package ru.practicum.shareit.booking.service; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.service.handler.booker.BookingStateHandler; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class BookerStateProcessor { + private final List handlers; + + public BookerStateProcessor(@Qualifier("booker") List handlers) { + this.handlers = handlers; + } + + public List process(BookingState state, Long userId, LocalDateTime now) { + return handlers.stream() + .filter(handler -> handler.canHandle(state)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Неизвестный статус: " + state)) + .getBookings(userId, now); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/service/BookingService.java b/src/main/java/ru/practicum/shareit/booking/service/BookingService.java new file mode 100644 index 0000000..69b502c --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/BookingService.java @@ -0,0 +1,17 @@ +package ru.practicum.shareit.booking.service; + +import ru.practicum.shareit.booking.dto.BookingDto; + +import java.util.List; + +public interface BookingService { + BookingDto create(BookingDto bookingDto, Long userId); + + BookingDto approve(Long bookingId, Long userId, Boolean approved); + + BookingDto getById(Long bookingId, Long userId); + + List getAllByBooker(Long userId, String state); + + List getAllByOwner(Long userId, String state); +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/service/handler/AbstractBookingStateHandler.java b/src/main/java/ru/practicum/shareit/booking/service/handler/AbstractBookingStateHandler.java new file mode 100644 index 0000000..ed87be0 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/handler/AbstractBookingStateHandler.java @@ -0,0 +1,23 @@ +package ru.practicum.shareit.booking.service.handler; + +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.booking.service.handler.booker.BookingStateHandler; + +import java.time.LocalDateTime; +import java.util.List; + +public abstract class AbstractBookingStateHandler implements BookingStateHandler { + protected final BookingRepository bookingRepository; + + public AbstractBookingStateHandler(BookingRepository bookingRepository) { + this.bookingRepository = bookingRepository; + } + + @Override + public abstract boolean canHandle(BookingState state); + + @Override + public abstract List getBookings(Long userId, LocalDateTime now); +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/service/handler/booker/AllBookingsHandler.java b/src/main/java/ru/practicum/shareit/booking/service/handler/booker/AllBookingsHandler.java new file mode 100644 index 0000000..9b567c6 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/handler/booker/AllBookingsHandler.java @@ -0,0 +1,32 @@ +package ru.practicum.shareit.booking.service.handler.booker; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.booking.service.handler.AbstractBookingStateHandler; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@Qualifier("booker") +@Order(1) +public class AllBookingsHandler extends AbstractBookingStateHandler { + + public AllBookingsHandler(BookingRepository bookingRepository) { + super(bookingRepository); + } + + @Override + public boolean canHandle(BookingState state) { + return state == BookingState.ALL; + } + + @Override + public List getBookings(Long userId, LocalDateTime now) { + return bookingRepository.findAllByBookerId(userId); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/service/handler/booker/BookingStateHandler.java b/src/main/java/ru/practicum/shareit/booking/service/handler/booker/BookingStateHandler.java new file mode 100644 index 0000000..c1201ea --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/handler/booker/BookingStateHandler.java @@ -0,0 +1,13 @@ +package ru.practicum.shareit.booking.service.handler.booker; + +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.model.Booking; + +import java.time.LocalDateTime; +import java.util.List; + +public interface BookingStateHandler { + boolean canHandle(BookingState state); + + List getBookings(Long userId, LocalDateTime now); +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/service/handler/booker/CurrentBookingsHandler.java b/src/main/java/ru/practicum/shareit/booking/service/handler/booker/CurrentBookingsHandler.java new file mode 100644 index 0000000..811a73b --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/handler/booker/CurrentBookingsHandler.java @@ -0,0 +1,32 @@ +package ru.practicum.shareit.booking.service.handler.booker; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.booking.service.handler.AbstractBookingStateHandler; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@Qualifier("booker") +@Order(2) +public class CurrentBookingsHandler extends AbstractBookingStateHandler { + + public CurrentBookingsHandler(BookingRepository bookingRepository) { + super(bookingRepository); + } + + @Override + public boolean canHandle(BookingState state) { + return state == BookingState.CURRENT; + } + + @Override + public List getBookings(Long userId, LocalDateTime now) { + return bookingRepository.findAllByBookerIdAndStartBeforeAndEndAfter(userId, now, now); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/service/handler/booker/FutureBookingsHandler.java b/src/main/java/ru/practicum/shareit/booking/service/handler/booker/FutureBookingsHandler.java new file mode 100644 index 0000000..7009733 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/handler/booker/FutureBookingsHandler.java @@ -0,0 +1,32 @@ +package ru.practicum.shareit.booking.service.handler.booker; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.booking.service.handler.AbstractBookingStateHandler; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@Qualifier("booker") +@Order(4) +public class FutureBookingsHandler extends AbstractBookingStateHandler { + + public FutureBookingsHandler(BookingRepository bookingRepository) { + super(bookingRepository); + } + + @Override + public boolean canHandle(BookingState state) { + return state == BookingState.FUTURE; + } + + @Override + public List getBookings(Long userId, LocalDateTime now) { + return bookingRepository.findAllByBookerIdAndStartAfter(userId, now); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/service/handler/booker/PastBookingsHandler.java b/src/main/java/ru/practicum/shareit/booking/service/handler/booker/PastBookingsHandler.java new file mode 100644 index 0000000..fc36197 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/handler/booker/PastBookingsHandler.java @@ -0,0 +1,32 @@ +package ru.practicum.shareit.booking.service.handler.booker; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.booking.service.handler.AbstractBookingStateHandler; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@Qualifier("booker") +@Order(3) +public class PastBookingsHandler extends AbstractBookingStateHandler { + + public PastBookingsHandler(BookingRepository bookingRepository) { + super(bookingRepository); + } + + @Override + public boolean canHandle(BookingState state) { + return state == BookingState.PAST; + } + + @Override + public List getBookings(Long userId, LocalDateTime now) { + return bookingRepository.findAllByBookerIdAndEndBefore(userId, now); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/service/handler/booker/RejectedBookingsHandler.java b/src/main/java/ru/practicum/shareit/booking/service/handler/booker/RejectedBookingsHandler.java new file mode 100644 index 0000000..5b51d17 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/handler/booker/RejectedBookingsHandler.java @@ -0,0 +1,33 @@ +package ru.practicum.shareit.booking.service.handler.booker; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.BookingStatus; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.booking.service.handler.AbstractBookingStateHandler; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@Qualifier("booker") +@Order(6) +public class RejectedBookingsHandler extends AbstractBookingStateHandler { + + public RejectedBookingsHandler(BookingRepository bookingRepository) { + super(bookingRepository); + } + + @Override + public boolean canHandle(BookingState state) { + return state == BookingState.REJECTED; + } + + @Override + public List getBookings(Long userId, LocalDateTime now) { + return bookingRepository.findAllByBookerIdAndStatus(userId, BookingStatus.REJECTED); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/service/handler/booker/WaitingBookingsHandler.java b/src/main/java/ru/practicum/shareit/booking/service/handler/booker/WaitingBookingsHandler.java new file mode 100644 index 0000000..1f22ace --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/handler/booker/WaitingBookingsHandler.java @@ -0,0 +1,33 @@ +package ru.practicum.shareit.booking.service.handler.booker; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.BookingStatus; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.booking.service.handler.AbstractBookingStateHandler; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@Qualifier("booker") +@Order(5) +public class WaitingBookingsHandler extends AbstractBookingStateHandler { + + public WaitingBookingsHandler(BookingRepository bookingRepository) { + super(bookingRepository); + } + + @Override + public boolean canHandle(BookingState state) { + return state == BookingState.WAITING; + } + + @Override + public List getBookings(Long userId, LocalDateTime now) { + return bookingRepository.findAllByBookerIdAndStatus(userId, BookingStatus.WAITING); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerAllBookingsHandler.java b/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerAllBookingsHandler.java new file mode 100644 index 0000000..175a8d8 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerAllBookingsHandler.java @@ -0,0 +1,32 @@ +package ru.practicum.shareit.booking.service.handler.owner; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.booking.service.handler.AbstractBookingStateHandler; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@Qualifier("owner") +@Order(1) +public class OwnerAllBookingsHandler extends AbstractBookingStateHandler { + + public OwnerAllBookingsHandler(BookingRepository bookingRepository) { + super(bookingRepository); + } + + @Override + public boolean canHandle(BookingState state) { + return state == BookingState.ALL; + } + + @Override + public List getBookings(Long userId, LocalDateTime now) { + return bookingRepository.findAllByItemOwnerId(userId); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerCurrentBookingsHandler.java b/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerCurrentBookingsHandler.java new file mode 100644 index 0000000..2c8445d --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerCurrentBookingsHandler.java @@ -0,0 +1,32 @@ +package ru.practicum.shareit.booking.service.handler.owner; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.booking.service.handler.AbstractBookingStateHandler; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@Qualifier("owner") +@Order(2) +public class OwnerCurrentBookingsHandler extends AbstractBookingStateHandler { + + public OwnerCurrentBookingsHandler(BookingRepository bookingRepository) { + super(bookingRepository); + } + + @Override + public boolean canHandle(BookingState state) { + return state == BookingState.CURRENT; + } + + @Override + public List getBookings(Long userId, LocalDateTime now) { + return bookingRepository.findAllByItemOwnerIdAndStartBeforeAndEndAfter(userId, now, now); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerFutureBookingsHandler.java b/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerFutureBookingsHandler.java new file mode 100644 index 0000000..9dcdddd --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerFutureBookingsHandler.java @@ -0,0 +1,32 @@ +package ru.practicum.shareit.booking.service.handler.owner; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.booking.service.handler.AbstractBookingStateHandler; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@Qualifier("owner") +@Order(4) +public class OwnerFutureBookingsHandler extends AbstractBookingStateHandler { + + public OwnerFutureBookingsHandler(BookingRepository bookingRepository) { + super(bookingRepository); + } + + @Override + public boolean canHandle(BookingState state) { + return state == BookingState.FUTURE; + } + + @Override + public List getBookings(Long userId, LocalDateTime now) { + return bookingRepository.findAllByItemOwnerIdAndStartAfter(userId, now); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerPastBookingsHandler.java b/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerPastBookingsHandler.java new file mode 100644 index 0000000..668f2d6 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerPastBookingsHandler.java @@ -0,0 +1,32 @@ +package ru.practicum.shareit.booking.service.handler.owner; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.booking.service.handler.AbstractBookingStateHandler; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@Qualifier("owner") +@Order(3) +public class OwnerPastBookingsHandler extends AbstractBookingStateHandler { + + public OwnerPastBookingsHandler(BookingRepository bookingRepository) { + super(bookingRepository); + } + + @Override + public boolean canHandle(BookingState state) { + return state == BookingState.PAST; + } + + @Override + public List getBookings(Long userId, LocalDateTime now) { + return bookingRepository.findAllByItemOwnerIdAndEndBefore(userId, now); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerRejectedBookingsHandler.java b/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerRejectedBookingsHandler.java new file mode 100644 index 0000000..6db61cb --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerRejectedBookingsHandler.java @@ -0,0 +1,33 @@ +package ru.practicum.shareit.booking.service.handler.owner; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.BookingStatus; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.booking.service.handler.AbstractBookingStateHandler; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@Qualifier("owner") +@Order(6) +public class OwnerRejectedBookingsHandler extends AbstractBookingStateHandler { + + public OwnerRejectedBookingsHandler(BookingRepository bookingRepository) { + super(bookingRepository); + } + + @Override + public boolean canHandle(BookingState state) { + return state == BookingState.REJECTED; + } + + @Override + public List getBookings(Long userId, LocalDateTime now) { + return bookingRepository.findAllByItemOwnerIdAndStatus(userId, BookingStatus.REJECTED); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerStateProcessor.java b/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerStateProcessor.java new file mode 100644 index 0000000..e796add --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerStateProcessor.java @@ -0,0 +1,27 @@ +package ru.practicum.shareit.booking.service.handler.owner; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.service.handler.booker.BookingStateHandler; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class OwnerStateProcessor { + private final List handlers; + + public OwnerStateProcessor(@Qualifier("owner") List handlers) { + this.handlers = handlers; + } + + public List process(BookingState state, Long userId, LocalDateTime now) { + return handlers.stream() + .filter(handler -> handler.canHandle(state)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Неизвестный статус: " + state)) + .getBookings(userId, now); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerWaitingBookingsHandler.java b/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerWaitingBookingsHandler.java new file mode 100644 index 0000000..3ae788a --- /dev/null +++ b/src/main/java/ru/practicum/shareit/booking/service/handler/owner/OwnerWaitingBookingsHandler.java @@ -0,0 +1,33 @@ +package ru.practicum.shareit.booking.service.handler.owner; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.BookingStatus; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.booking.service.handler.AbstractBookingStateHandler; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@Qualifier("owner") +@Order(5) +public class OwnerWaitingBookingsHandler extends AbstractBookingStateHandler { + + public OwnerWaitingBookingsHandler(BookingRepository bookingRepository) { + super(bookingRepository); + } + + @Override + public boolean canHandle(BookingState state) { + return state == BookingState.WAITING; + } + + @Override + public List getBookings(Long userId, LocalDateTime now) { + return bookingRepository.findAllByItemOwnerIdAndStatus(userId, BookingStatus.WAITING); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/CommentMapper.java b/src/main/java/ru/practicum/shareit/item/CommentMapper.java new file mode 100644 index 0000000..25c2dc8 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/CommentMapper.java @@ -0,0 +1,26 @@ +package ru.practicum.shareit.item; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.NullValuePropertyMappingStrategy; +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.model.Comment; + +import java.time.LocalDateTime; + +@Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) +public interface CommentMapper { + + @Mapping(target = "authorName", source = "author.name") + CommentDto toCommentDto(Comment comment); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "author", ignore = true) + @Mapping(target = "item", ignore = true) + @Mapping(target = "created", expression = "java(getCurrentTime())") + Comment toComment(CommentDto commentDto); + + default LocalDateTime getCurrentTime() { + return LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/Impl/ItemRepositoryImpl.java b/src/main/java/ru/practicum/shareit/item/Impl/ItemRepositoryImpl.java deleted file mode 100644 index 8bb6974..0000000 --- a/src/main/java/ru/practicum/shareit/item/Impl/ItemRepositoryImpl.java +++ /dev/null @@ -1,34 +0,0 @@ -package ru.practicum.shareit.item.Impl; - -import org.springframework.stereotype.Component; -import ru.practicum.shareit.item.model.Item; -import ru.practicum.shareit.item.repository.AbstractRepository; -import ru.practicum.shareit.item.repository.ItemRepository; - -@Component -public class ItemRepositoryImpl extends AbstractRepository implements ItemRepository { - - @Override - public Item create(Item item) { - setEntityId(item, nextId); - nextId++; - entities.put(item.getId(), item); - return item; - } - - @Override - public Item update(Item item) { - entities.put(item.getId(), item); - return item; - } - - @Override - protected void setEntityId(Item entity, Long id) { - entity.setId(id); - } - - @Override - protected Long getEntityId(Item entity) { - return entity.getId(); - } -} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/Impl/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/Impl/ItemServiceImpl.java index 0527ae9..e292c25 100644 --- a/src/main/java/ru/practicum/shareit/item/Impl/ItemServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/item/Impl/ItemServiceImpl.java @@ -1,60 +1,127 @@ package ru.practicum.shareit.item.Impl; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.booking.BookingMapper; import ru.practicum.shareit.booking.exception.ShareItException; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.item.CommentMapper; import ru.practicum.shareit.item.ItemMapper; +import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.model.Comment; import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.CommentRepository; import ru.practicum.shareit.item.repository.ItemRepository; import ru.practicum.shareit.item.service.ItemService; import ru.practicum.shareit.user.model.User; import ru.practicum.shareit.user.repository.UserRepository; +import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Service +@RequiredArgsConstructor +@Transactional(readOnly = true) public class ItemServiceImpl implements ItemService { private final ItemRepository itemRepository; private final UserRepository userRepository; + private final BookingRepository bookingRepository; + private final CommentRepository commentRepository; private final ItemMapper itemMapper; - - public ItemServiceImpl(ItemRepository itemRepository, UserRepository userRepository, ItemMapper itemMapper) { - this.itemRepository = itemRepository; - this.userRepository = userRepository; - this.itemMapper = itemMapper; - } + private final CommentMapper commentMapper; + private final BookingMapper bookingMapper; @Override public List getAll(Long userId) { - return itemRepository.findAll().stream() - .filter(item -> item.getOwner().getId().equals(userId)) - .map(itemMapper::toItemDto) + List items = itemRepository.findByOwnerId(userId); + List itemIds = items.stream() + .map(Item::getId) + .collect(Collectors.toList()); + + Map> commentsByItemId = commentRepository.findByItemIdIn(itemIds).stream() + .collect(Collectors.groupingBy(comment -> comment.getItem().getId())); + + LocalDateTime now = LocalDateTime.now(); + + return items.stream() + .map(item -> { + ItemDto itemDto = itemMapper.toItemDto(item); + + if (item.getOwner().getId().equals(userId)) { + List lastBookings = bookingRepository.findLastBookingForItem(item.getId(), now); + if (!lastBookings.isEmpty()) { + itemDto.setLastBooking(bookingMapper.toBookingShortDto(lastBookings.get(0))); + } + + List nextBookings = bookingRepository.findNextBookingForItem(item.getId(), now); + if (!nextBookings.isEmpty()) { + itemDto.setNextBooking(bookingMapper.toBookingShortDto(nextBookings.get(0))); + } + } + + List comments = commentsByItemId.getOrDefault(item.getId(), Collections.emptyList()); + itemDto.setComments(comments.stream() + .map(commentMapper::toCommentDto) + .collect(Collectors.toList())); + + return itemDto; + }) .collect(Collectors.toList()); } @Override - public ItemDto getById(Long id) { + public ItemDto getById(Long id, Long userId) { Item item = itemRepository.findById(id) .orElseThrow(() -> new ShareItException.NotFoundException("Не найдена вещь с id: " + id)); - return itemMapper.toItemDto(item); + ItemDto itemDto = itemMapper.toItemDto(item); + + LocalDateTime now = LocalDateTime.now(); + if (item.getOwner().getId().equals(userId)) { + List lastBookings = bookingRepository.findLastBookingForItem(id, now); + if (!lastBookings.isEmpty()) { + itemDto.setLastBooking(bookingMapper.toBookingShortDto(lastBookings.get(0))); + } + + List nextBookings = bookingRepository.findNextBookingForItem(id, now); + if (!nextBookings.isEmpty()) { + itemDto.setNextBooking(bookingMapper.toBookingShortDto(nextBookings.get(0))); + } + } + + List comments = commentRepository.findByItemId(id); + itemDto.setComments(comments.stream() + .map(commentMapper::toCommentDto) + .collect(Collectors.toList())); + + return itemDto; } @Override + @Transactional public ItemDto create(ItemDto itemDto, Long userId) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new ShareItException.NotFoundException("Невозможно создать вещь - " + - "не найден пользователь с id: " + userId)); + User user = getUserById(userId); // Отдельный метод без транзакции Item item = itemMapper.toItem(itemDto); item.setOwner(user); - itemRepository.create(item); - return itemMapper.toItemDto(item); + return itemMapper.toItemDto(itemRepository.save(item)); + } + + @Transactional(readOnly = true) + private User getUserById(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new ShareItException.NotFoundException("Не найден пользователь с id: " + userId)); } @Override + @Transactional public ItemDto update(ItemDto itemDto, Long id, Long userId) { Item item = itemRepository.findById(id) .orElseThrow(() -> new ShareItException.NotFoundException("Не найдена вещь с id: " + id)); @@ -64,13 +131,15 @@ public ItemDto update(ItemDto itemDto, Long id, Long userId) { } Item updatedItem = itemMapper.updateItemFields(item, itemDto); - return itemMapper.toItemDto(itemRepository.update(updatedItem)); + return itemMapper.toItemDto(itemRepository.save(updatedItem)); } @Override + @Transactional public void delete(Long id) { - getById(id); - itemRepository.delete(id); + itemRepository.findById(id) + .orElseThrow(() -> new ShareItException.NotFoundException("Не найдена вещь с id: " + id)); + itemRepository.deleteById(id); } @Override @@ -79,12 +148,31 @@ public List search(String text) { return new ArrayList<>(); } - String searchText = text.toLowerCase(); - return itemRepository.findAll().stream() - .filter(item -> item.getAvailable() && - (item.getName().toLowerCase().contains(searchText) || - item.getDescription().toLowerCase().contains(searchText))) + return itemRepository.search(text).stream() .map(itemMapper::toItemDto) .collect(Collectors.toList()); } + + @Override + @Transactional + public CommentDto createComment(Long itemId, CommentDto commentDto, Long userId) { + Item item = itemRepository.findById(itemId) + .orElseThrow(() -> new ShareItException.NotFoundException("Не найдена вещь с id: " + itemId)); + + User user = userRepository.findById(userId) + .orElseThrow(() -> new ShareItException.NotFoundException("Не найден пользователь с id: " + userId)); + + boolean hasBookedItem = bookingRepository.hasUserBookedItem(userId, itemId, LocalDateTime.now()); + + if (!hasBookedItem) { + throw new ShareItException.BadRequestException("Пользователь не может оставить отзыв, так как не брал вещь в аренду или аренда еще не завершена"); + } + + Comment comment = commentMapper.toComment(commentDto); + comment.setItem(item); + comment.setAuthor(user); + comment.setCreated(LocalDateTime.now()); + + return commentMapper.toCommentDto(commentRepository.save(comment)); + } } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/ItemMapper.java index ece92a4..d1e0b94 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemMapper.java +++ b/src/main/java/ru/practicum/shareit/item/ItemMapper.java @@ -7,11 +7,15 @@ import ru.practicum.shareit.item.dto.ItemDto; import ru.practicum.shareit.item.model.Item; - -@Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) +@Mapper(componentModel = "spring", + nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE, + uses = {CommentMapper.class}) public interface ItemMapper { @Mapping(target = "request", expression = "java(item.getRequest() != null ? item.getRequest() : null)") + @Mapping(target = "lastBooking", ignore = true) + @Mapping(target = "nextBooking", ignore = true) + @Mapping(target = "comments", ignore = true) ItemDto toItemDto(Item item); Item toItem(ItemDto itemDto); diff --git a/src/main/java/ru/practicum/shareit/item/controller/ItemController.java b/src/main/java/ru/practicum/shareit/item/controller/ItemController.java index d00b6fb..9d2c2ff 100644 --- a/src/main/java/ru/practicum/shareit/item/controller/ItemController.java +++ b/src/main/java/ru/practicum/shareit/item/controller/ItemController.java @@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; import ru.practicum.shareit.item.service.ItemService; import ru.practicum.shareit.item.util.Constants; @@ -32,8 +33,8 @@ public List getAll(@RequestHeader(Constants.USER_ID_HEADER) Long userId } @GetMapping("/{id}") - public ItemDto getById(@PathVariable Long id) { - return itemService.getById(id); + public ItemDto getById(@PathVariable Long id, @RequestHeader(Constants.USER_ID_HEADER) Long userId) { + return itemService.getById(id, userId); } @PostMapping @@ -56,4 +57,11 @@ public void delete(@PathVariable Long id) { public List search(@RequestParam String text) { return itemService.search(text); } + + @PostMapping("/{itemId}/comment") + public CommentDto createComment(@PathVariable Long itemId, + @Valid @RequestBody CommentDto commentDto, + @RequestHeader(Constants.USER_ID_HEADER) Long userId) { + return itemService.createComment(itemId, commentDto, userId); + } } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java new file mode 100644 index 0000000..66bcda2 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java @@ -0,0 +1,22 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Builder(toBuilder = true) +public class CommentDto { + private Long id; + + @NotBlank + private String text; + + private String authorName; + + private LocalDateTime created; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java index ed1ea71..095dc52 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -5,8 +5,11 @@ import lombok.Builder; import lombok.Getter; import lombok.Setter; +import ru.practicum.shareit.booking.dto.BookingShortDto; import ru.practicum.shareit.request.model.ItemRequest; +import java.util.ArrayList; +import java.util.List; @Getter @Setter @@ -24,4 +27,13 @@ public class ItemDto { private Boolean available; private ItemRequest request; + + private BookingShortDto lastBooking; + + private BookingShortDto nextBooking; + + @Builder.Default + private List comments = new ArrayList<>(); + + private Long requestId; } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/model/Comment.java b/src/main/java/ru/practicum/shareit/item/model/Comment.java new file mode 100644 index 0000000..4964d29 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/model/Comment.java @@ -0,0 +1,48 @@ +package ru.practicum.shareit.item.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "comments") +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank + @Column(name = "text", nullable = false) + private String text; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_id", nullable = false) + private Item item; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "author_id", nullable = false) + private User author; + + @Column(name = "created", nullable = false) + private LocalDateTime created; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/model/Item.java b/src/main/java/ru/practicum/shareit/item/model/Item.java index 7021c25..d9a973e 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -1,30 +1,53 @@ package ru.practicum.shareit.item.model; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import ru.practicum.shareit.request.model.ItemRequest; import ru.practicum.shareit.user.model.User; - +@Entity +@Table(name = "items") @Getter @Setter @Builder +@NoArgsConstructor +@AllArgsConstructor public class Item { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank + @Column(name = "name", nullable = false) private String name; @NotBlank + @Column(name = "description", nullable = false) private String description; @NotNull + @Column(name = "available", nullable = false) private Boolean available; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "owner_id", nullable = false) private User owner; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "request_id") private ItemRequest request; } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/repository/AbstractRepository.java b/src/main/java/ru/practicum/shareit/item/repository/AbstractRepository.java deleted file mode 100644 index 4f5dd72..0000000 --- a/src/main/java/ru/practicum/shareit/item/repository/AbstractRepository.java +++ /dev/null @@ -1,28 +0,0 @@ -package ru.practicum.shareit.item.repository; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -public abstract class AbstractRepository { - protected final Map entities = new HashMap<>(); - protected Long nextId = 1L; - - public List findAll() { - return new ArrayList<>(entities.values()); - } - - public Optional findById(K id) { - return entities.containsKey(id) ? Optional.of(entities.get(id)) : Optional.empty(); - } - - public void delete(K id) { - entities.remove(id); - } - - protected abstract void setEntityId(T entity, Long id); - - protected abstract K getEntityId(T entity); -} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java b/src/main/java/ru/practicum/shareit/item/repository/CommentRepository.java new file mode 100644 index 0000000..570a0a2 --- /dev/null +++ b/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.List; + +public interface CommentRepository extends JpaRepository { + + List findByItemId(Long itemId); + + List findByItemIdIn(List itemIds); +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java b/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java index 9bf5801..5f0abcb 100644 --- a/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java +++ b/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java @@ -1,18 +1,19 @@ package ru.practicum.shareit.item.repository; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import ru.practicum.shareit.item.model.Item; import java.util.List; -import java.util.Optional; -public interface ItemRepository { - List findAll(); +public interface ItemRepository extends JpaRepository { - Optional findById(Long id); + List findByOwnerId(Long ownerId); - Item create(Item item); - - Item update(Item item); - - void delete(Long id); + @Query("SELECT i FROM Item i " + + "WHERE i.available = true AND " + + "(LOWER(i.name) LIKE LOWER(CONCAT('%', :text, '%')) OR " + + "LOWER(i.description) LIKE LOWER(CONCAT('%', :text, '%')))") + List search(@Param("text") String text); } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/service/ItemService.java b/src/main/java/ru/practicum/shareit/item/service/ItemService.java index 07f58b1..490ca15 100644 --- a/src/main/java/ru/practicum/shareit/item/service/ItemService.java +++ b/src/main/java/ru/practicum/shareit/item/service/ItemService.java @@ -1,5 +1,6 @@ package ru.practicum.shareit.item.service; +import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; import java.util.List; @@ -7,7 +8,7 @@ public interface ItemService { List getAll(Long userId); - ItemDto getById(Long id); + ItemDto getById(Long id, Long userId); ItemDto create(ItemDto itemDto, Long userId); @@ -16,4 +17,6 @@ public interface ItemService { void delete(Long id); List search(String text); + + CommentDto createComment(Long itemId, CommentDto commentDto, Long userId); } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java index 5612dda..9ee9653 100644 --- a/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java +++ b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java @@ -1,22 +1,44 @@ package ru.practicum.shareit.request.model; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import ru.practicum.shareit.user.model.User; import java.time.LocalDateTime; - +@Entity +@Table(name = "item_requests") @Getter @Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor public class ItemRequest { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank + @Column(name = "description", nullable = false) private String description; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "requestor_id", nullable = false) private User requestor; + @Column(name = "created", nullable = false) private LocalDateTime created; } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/Impl/UserRepositoryImpl.java b/src/main/java/ru/practicum/shareit/user/Impl/UserRepositoryImpl.java deleted file mode 100644 index 5f895be..0000000 --- a/src/main/java/ru/practicum/shareit/user/Impl/UserRepositoryImpl.java +++ /dev/null @@ -1,34 +0,0 @@ -package ru.practicum.shareit.user.Impl; - -import org.springframework.stereotype.Component; -import ru.practicum.shareit.item.repository.AbstractRepository; -import ru.practicum.shareit.user.model.User; -import ru.practicum.shareit.user.repository.UserRepository; - -@Component -public class UserRepositoryImpl extends AbstractRepository implements UserRepository { - - @Override - public User create(User user) { - setEntityId(user, nextId); - nextId++; - entities.put(user.getId(), user); - return user; - } - - @Override - public User update(User user) { - entities.put(user.getId(), user); - return user; - } - - @Override - protected void setEntityId(User entity, Long id) { - entity.setId(id); - } - - @Override - protected Long getEntityId(User entity) { - return entity.getId(); - } -} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/Impl/UserServiceImpl.java b/src/main/java/ru/practicum/shareit/user/Impl/UserServiceImpl.java index ac0e48b..a53beab 100644 --- a/src/main/java/ru/practicum/shareit/user/Impl/UserServiceImpl.java +++ b/src/main/java/ru/practicum/shareit/user/Impl/UserServiceImpl.java @@ -1,6 +1,9 @@ package ru.practicum.shareit.user.Impl; +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import ru.practicum.shareit.booking.exception.ShareItException; import ru.practicum.shareit.user.UserMapper; import ru.practicum.shareit.user.dto.UserDto; @@ -8,21 +11,15 @@ import ru.practicum.shareit.user.repository.UserRepository; import ru.practicum.shareit.user.service.UserService; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.stream.Collectors; @Service +@RequiredArgsConstructor +@Transactional(readOnly = true) public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final UserMapper userMapper; - private final Set emailSet = new HashSet<>(); - - public UserServiceImpl(UserRepository userRepository, UserMapper userMapper) { - this.userRepository = userRepository; - this.userMapper = userMapper; - } @Override public List getAll() { @@ -35,50 +32,41 @@ public List getAll() { public UserDto getById(Long id) { User user = userRepository.findById(id) .orElseThrow(() -> new ShareItException.NotFoundException("Не найден пользователь с id: " + id)); - return userMapper.toUserDto(user); } @Override + @Transactional public UserDto create(User user) { - checkEmailUniqueness(user); - User createdUser = userRepository.create(user); - emailSet.add(createdUser.getEmail().toLowerCase()); - return userMapper.toUserDto(createdUser); + try { + User createdUser = userRepository.save(user); + return userMapper.toUserDto(createdUser); + } catch (DataIntegrityViolationException e) { + throw new ShareItException.ConflictException("Пользователь с таким email уже зарегистрирован"); + } } @Override + @Transactional public UserDto update(User user, Long id) { - User updatedUser = userRepository.findById(id) + User existingUser = userRepository.findById(id) .orElseThrow(() -> new ShareItException.NotFoundException("Невозможно обновить данные пользователя. " + "Не найден пользователь с id: " + id)); - if (user.getEmail() != null && !user.getEmail().equals(updatedUser.getEmail())) { - checkEmailUniqueness(user); - emailSet.remove(updatedUser.getEmail().toLowerCase()); - emailSet.add(user.getEmail().toLowerCase()); + try { + User updated = userMapper.updateUserFields(existingUser, userMapper.toUserDto(user)); + return userMapper.toUserDto(userRepository.save(updated)); + } catch (DataIntegrityViolationException e) { + throw new ShareItException.ConflictException("Пользователь с таким email уже зарегистрирован"); } - - User updated = userMapper.updateUserFields(updatedUser, userMapper.toUserDto(user)); - return userMapper.toUserDto(userRepository.update(updated)); } @Override + @Transactional public void delete(Long id) { - User user = userRepository.findById(id) - .orElseThrow(() -> new ShareItException.NotFoundException("Не найден пользователь с id: " + id)); - emailSet.remove(user.getEmail().toLowerCase()); - userRepository.delete(id); - } - - private void checkEmailUniqueness(User user) { - if (user.getEmail() == null) { - return; - } - - String email = user.getEmail().toLowerCase(); - if (emailSet.contains(email)) { - throw new ShareItException.ConflictException("Пользователь с таким email уже зарегистрирован"); + if (!userRepository.existsById(id)) { + throw new ShareItException.NotFoundException("Не найден пользователь с id: " + id); } + userRepository.deleteById(id); } } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/model/User.java b/src/main/java/ru/practicum/shareit/user/model/User.java index 9647105..7c29a66 100644 --- a/src/main/java/ru/practicum/shareit/user/model/User.java +++ b/src/main/java/ru/practicum/shareit/user/model/User.java @@ -1,22 +1,37 @@ package ru.practicum.shareit.user.model; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; - +@Entity +@Table(name = "users") @Getter @Setter @Builder +@NoArgsConstructor +@AllArgsConstructor public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank + @Column(name = "name", nullable = false) private String name; @Email @NotBlank + @Column(name = "email", nullable = false, unique = true) private String email; } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java b/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java index 34465a7..2a1d8c2 100644 --- a/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java +++ b/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java @@ -1,18 +1,7 @@ package ru.practicum.shareit.user.repository; +import org.springframework.data.jpa.repository.JpaRepository; import ru.practicum.shareit.user.model.User; -import java.util.List; -import java.util.Optional; - -public interface UserRepository { - List findAll(); - - Optional findById(Long id); - - User create(User user); - - User update(User user); - - void delete(Long id); +public interface UserRepository extends JpaRepository { } \ No newline at end of file diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties new file mode 100644 index 0000000..6b60a07 --- /dev/null +++ b/src/main/resources/application-test.properties @@ -0,0 +1,9 @@ +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL +spring.datasource.username=test +spring.datasource.password=test +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.show-sql=true +spring.sql.init.mode=never diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b9e5d4b..929fb7e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,14 +1,13 @@ spring.jpa.hibernate.ddl-auto=none spring.jpa.properties.hibernate.format_sql=true spring.sql.init.mode=always - logging.level.org.springframework.orm.jpa=INFO logging.level.org.springframework.transaction=INFO logging.level.org.springframework.transaction.interceptor=TRACE logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG +spring.datasource.driverClassName=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://localhost:5432/shareit +spring.datasource.username=postgres +spring.datasource.password=viktor -# TODO Append connection to DB -#spring.datasource.driverClassName -#spring.datasource.url -#spring.datasource.username -#spring.datasource.password +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..df4f228 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,8 @@ +-- Проверка существования перед вставкой +INSERT INTO users (name, email) +SELECT 'User 1', 'user1@example.com' +WHERE NOT EXISTS (SELECT 1 FROM users WHERE email = 'user1@example.com'); + +INSERT INTO users (name, email) +SELECT 'User 2', 'user2@example.com' +WHERE NOT EXISTS (SELECT 1 FROM users WHERE email = 'user2@example.com'); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..2d6459a --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,54 @@ +CREATE TABLE IF NOT EXISTS users ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE +); + +CREATE TABLE IF NOT EXISTS item_requests ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + description VARCHAR(1000) NOT NULL, + requestor_id BIGINT NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + CONSTRAINT fk_requests_to_users FOREIGN KEY(requestor_id) REFERENCES users(id) +); + +CREATE TABLE IF NOT EXISTS items ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description VARCHAR(512) NOT NULL, + available BOOLEAN NOT NULL, + owner_id BIGINT NOT NULL, + request_id BIGINT, + CONSTRAINT fk_item_owner FOREIGN KEY (owner_id) REFERENCES users (id), + CONSTRAINT fk_item_request FOREIGN KEY (request_id) REFERENCES item_requests (id) +); + +CREATE TABLE IF NOT EXISTS bookings ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + start_date TIMESTAMP WITHOUT TIME ZONE NOT NULL, + end_date TIMESTAMP WITHOUT TIME ZONE NOT NULL, + item_id BIGINT NOT NULL, + booker_id BIGINT NOT NULL, + status VARCHAR(50) NOT NULL, + CONSTRAINT fk_bookings_to_items FOREIGN KEY(item_id) REFERENCES items(id), + CONSTRAINT fk_bookings_to_users FOREIGN KEY(booker_id) REFERENCES users(id) +); + +-- Индекс для поиска бронирований по вещи (часто используется в JOIN) +CREATE INDEX IF NOT EXISTS idx_bookings_item ON bookings(item_id); +-- Индекс для поиска бронирований по пользователю (используется в WHERE) +CREATE INDEX IF NOT EXISTS idx_bookings_booker ON bookings(booker_id); +-- Индекс для поиска бронирований по статусу (используется в WHERE) +CREATE INDEX IF NOT EXISTS idx_bookings_status ON bookings(status); +-- Составной индекс для поиска бронирований по дате начала и окончания (используется в WHERE и ORDER BY) +CREATE INDEX IF NOT EXISTS idx_bookings_dates ON bookings(start_date, end_date); + +CREATE TABLE IF NOT EXISTS comments ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + text VARCHAR(1000) NOT NULL, + item_id BIGINT NOT NULL, + author_id BIGINT NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + CONSTRAINT fk_comments_to_items FOREIGN KEY(item_id) REFERENCES items(id), + CONSTRAINT fk_comments_to_users FOREIGN KEY(author_id) REFERENCES users(id) +); \ No newline at end of file diff --git a/src/test/java/ru/practicum/shareit/ShareItTests.java b/src/test/java/ru/practicum/shareit/ShareItTests.java index 4d79052..d53b14f 100644 --- a/src/test/java/ru/practicum/shareit/ShareItTests.java +++ b/src/test/java/ru/practicum/shareit/ShareItTests.java @@ -2,12 +2,15 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; @SpringBootTest +@ActiveProfiles("test") +@ContextConfiguration(classes = {ShareItApp.class, TestConfig.class}) class ShareItTests { - @Test - void contextLoads() { - } - -} + @Test + void contextLoads() { + } +} \ No newline at end of file diff --git a/src/test/java/ru/practicum/shareit/TestConfig.java b/src/test/java/ru/practicum/shareit/TestConfig.java new file mode 100644 index 0000000..42214b2 --- /dev/null +++ b/src/test/java/ru/practicum/shareit/TestConfig.java @@ -0,0 +1,22 @@ +package ru.practicum.shareit; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; + +import javax.sql.DataSource; + +@TestConfiguration +public class TestConfig { + + @Bean + @Primary + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .setName("testdb;MODE=PostgreSQL") + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/ru/practicum/shareit/booking/controller/BookingControllerTest.java b/src/test/java/ru/practicum/shareit/booking/controller/BookingControllerTest.java new file mode 100644 index 0000000..6c49a82 --- /dev/null +++ b/src/test/java/ru/practicum/shareit/booking/controller/BookingControllerTest.java @@ -0,0 +1,275 @@ +package ru.practicum.shareit.booking.controller; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.ShareItApp; +import ru.practicum.shareit.TestConfig; +import ru.practicum.shareit.booking.BookingStatus; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.exception.ShareItException; +import ru.practicum.shareit.item.controller.ItemController; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.user.UserMapper; +import ru.practicum.shareit.user.controller.UserController; +import ru.practicum.shareit.user.dto.UserDto; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ActiveProfiles("test") +@ContextConfiguration(classes = {ShareItApp.class, TestConfig.class}) +@SpringBootTest +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +class BookingControllerTest { + + @Autowired + private BookingController bookingController; + + @Autowired + private ItemController itemController; + + @Autowired + private UserController userController; + + @Autowired + private UserMapper userMapper; + + private UserDto ownerDto; + private UserDto bookerDto; + private ItemDto itemDto; + private BookingDto bookingDto; + + @BeforeEach + void setUp() { + String ownerEmail = "owner" + System.currentTimeMillis() + "@email.com"; + ownerDto = UserDto.builder() + .name("Item Owner") + .email(ownerEmail) + .build(); + ownerDto = userController.create(userMapper.toUser(ownerDto)); + + String bookerEmail = "booker" + System.currentTimeMillis() + "@email.com"; + bookerDto = UserDto.builder() + .name("Booker User") + .email(bookerEmail) + .build(); + bookerDto = userController.create(userMapper.toUser(bookerDto)); + + itemDto = ItemDto.builder() + .name("Test Item") + .description("Test Description") + .available(true) + .build(); + itemDto = itemController.create(ownerDto.getId(), itemDto); + + LocalDateTime start = LocalDateTime.now().plusDays(1); + LocalDateTime end = LocalDateTime.now().plusDays(2); + bookingDto = BookingDto.builder() + .itemId(itemDto.getId()) + .start(start) + .end(end) + .build(); + } + + @Nested // Тесты для создания бронирований + @DisplayName("Creating bookings") + class CreateBookingTests { + @Test + @Transactional + @DisplayName("Successful booking creation") + void createBookingTest() { + BookingDto createdBooking = bookingController.createBooking(bookingDto, bookerDto.getId()).getBody(); + + assertNotNull(createdBooking); + assertNotNull(createdBooking.getId()); + assertEquals(bookingDto.getStart(), createdBooking.getStart()); + assertEquals(bookingDto.getEnd(), createdBooking.getEnd()); + assertEquals(itemDto.getId(), createdBooking.getItem().getId()); + assertEquals(bookerDto.getId(), createdBooking.getBooker().getId()); + assertEquals(BookingStatus.WAITING, createdBooking.getStatus()); + } + + @Test + @Transactional + @DisplayName("Error when owner tries to book their own item") + void ownerBookingOwnItemTest() { + assertThrows(ShareItException.NotFoundException.class, + () -> bookingController.createBooking(bookingDto, ownerDto.getId())); + } + + @Test + @Transactional + @DisplayName("Error when booking unavailable item") + void bookingUnavailableItemTest() { + ItemDto updatedItem = ItemDto.builder() + .available(false) + .build(); + itemController.update(updatedItem, itemDto.getId(), ownerDto.getId()); + + assertThrows(ShareItException.BadRequestException.class, + () -> bookingController.createBooking(bookingDto, bookerDto.getId())); + } + + @Test + @Transactional + @DisplayName("Error when booking with invalid dates") + void bookingWithInvalidDatesTest() { + BookingDto invalidBooking = BookingDto.builder() + .itemId(itemDto.getId()) + .start(LocalDateTime.now().plusDays(2)) + .end(LocalDateTime.now().plusDays(1)) + .build(); + + assertThrows(ShareItException.BadRequestException.class, + () -> bookingController.createBooking(invalidBooking, bookerDto.getId())); + } + } + + @Nested // Тесты для утверждения бронирований + @DisplayName("Approving bookings") + class ApproveBookingTests { + @Test + @Transactional + @DisplayName("Successful booking approval") + void approveBookingTest() { + BookingDto createdBooking = bookingController.createBooking(bookingDto, bookerDto.getId()).getBody(); + BookingDto approvedBooking = bookingController.approve(createdBooking.getId(), ownerDto.getId(), true).getBody(); + + assertEquals(BookingStatus.APPROVED, approvedBooking.getStatus()); + } + + @Test + @Transactional + @DisplayName("Successful booking rejection") + void rejectBookingTest() { + BookingDto createdBooking = bookingController.createBooking(bookingDto, bookerDto.getId()).getBody(); + BookingDto rejectedBooking = bookingController.approve(createdBooking.getId(), ownerDto.getId(), false).getBody(); + + assertEquals(BookingStatus.REJECTED, rejectedBooking.getStatus()); + } + + @Test + @Transactional + @DisplayName("Error when non-owner tries to approve booking") + void nonOwnerApprovingBookingTest() { + BookingDto createdBooking = bookingController.createBooking(bookingDto, bookerDto.getId()).getBody(); + + String randomEmail = "random" + System.currentTimeMillis() + "@email.com"; + UserDto randomUser = UserDto.builder() + .name("Random User") + .email(randomEmail) + .build(); + randomUser = userController.create(userMapper.toUser(randomUser)); + + UserDto finalRandomUser = randomUser; + assertThrows(ShareItException.ForbiddenException.class, + () -> bookingController.approve(createdBooking.getId(), finalRandomUser.getId(), true)); + } + + @Test + @Transactional + @DisplayName("Error when approving already processed booking") + void approvingProcessedBookingTest() { + BookingDto createdBooking = bookingController.createBooking(bookingDto, bookerDto.getId()).getBody(); + bookingController.approve(createdBooking.getId(), ownerDto.getId(), true).getBody(); // Добавлен .getBody() + + assertThrows(ShareItException.BadRequestException.class, + () -> bookingController.approve(createdBooking.getId(), ownerDto.getId(), true)); + } + } + + @Nested // Тесты для получения бронирований + @DisplayName("Getting bookings") + class GetBookingTests { + @Test + @Transactional + @DisplayName("Getting booking by ID") + void getBookingByIdTest() { + BookingDto createdBooking = bookingController.createBooking(bookingDto, bookerDto.getId()).getBody(); + BookingDto retrievedBooking = bookingController.getById(createdBooking.getId(), bookerDto.getId()).getBody(); + + assertNotNull(retrievedBooking); + assertEquals(createdBooking.getId(), retrievedBooking.getId()); + assertEquals(createdBooking.getStart(), retrievedBooking.getStart()); + assertEquals(createdBooking.getEnd(), retrievedBooking.getEnd()); + } + + @Test + @Transactional + @DisplayName("Error when unauthorized user tries to get booking") + void unauthorizedGetBookingTest() { + BookingDto createdBooking = bookingController.createBooking(bookingDto, bookerDto.getId()).getBody(); + + String randomEmail = "random" + System.currentTimeMillis() + "@email.com"; + UserDto randomUser = UserDto.builder() + .name("Random User") + .email(randomEmail) + .build(); + randomUser = userController.create(userMapper.toUser(randomUser)); + + UserDto finalRandomUser = randomUser; + assertThrows(ShareItException.NotFoundException.class, + () -> bookingController.getById(createdBooking.getId(), finalRandomUser.getId())); + } + + @Test + @Transactional + @DisplayName("Getting all bookings by booker") + void getAllBookingsByBookerTest() { + bookingController.createBooking(bookingDto, bookerDto.getId()).getBody(); + + List bookings = bookingController.getAllByBooker(bookerDto.getId(), "ALL").getBody(); + + assertEquals(1, bookings.size()); + assertEquals(itemDto.getId(), bookings.get(0).getItem().getId()); + assertEquals(bookerDto.getId(), bookings.get(0).getBooker().getId()); + } + + @Test + @Transactional + @DisplayName("Getting all bookings by owner") + void getAllBookingsByOwnerTest() { + bookingController.createBooking(bookingDto, bookerDto.getId()).getBody(); + + List bookings = bookingController.getAllByOwner(ownerDto.getId(), "ALL").getBody(); + + assertEquals(1, bookings.size()); + assertEquals(itemDto.getId(), bookings.get(0).getItem().getId()); + assertEquals(bookerDto.getId(), bookings.get(0).getBooker().getId()); + } + + @Test + @Transactional + @DisplayName("Getting bookings with different states") + void getBookingsWithDifferentStatesTest() { + BookingDto createdBooking = bookingController.createBooking(bookingDto, bookerDto.getId()).getBody(); + bookingController.approve(createdBooking.getId(), ownerDto.getId(), true).getBody(); // Добавлен .getBody() + + List waitingBookings = bookingController.getAllByBooker(bookerDto.getId(), "WAITING").getBody(); + assertEquals(0, waitingBookings.size()); + + List futureBookings = bookingController.getAllByBooker(bookerDto.getId(), "FUTURE").getBody(); + assertEquals(1, futureBookings.size()); + } + + @Test + @Transactional + @DisplayName("Error when using invalid state") + void invalidStateTest() { + assertThrows(ShareItException.BadRequestException.class, + () -> bookingController.getAllByBooker(bookerDto.getId(), "INVALID_STATE")); + } + } +} \ No newline at end of file diff --git a/src/test/java/ru/practicum/shareit/booking/service/BookingServiceImplTest.java b/src/test/java/ru/practicum/shareit/booking/service/BookingServiceImplTest.java new file mode 100644 index 0000000..88050f9 --- /dev/null +++ b/src/test/java/ru/practicum/shareit/booking/service/BookingServiceImplTest.java @@ -0,0 +1,546 @@ +package ru.practicum.shareit.booking.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.shareit.ShareItApp; +import ru.practicum.shareit.TestConfig; +import ru.practicum.shareit.booking.BookingMapper; +import ru.practicum.shareit.booking.BookingState; +import ru.practicum.shareit.booking.BookingStatus; +import ru.practicum.shareit.booking.Impl.BookingServiceImpl; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.exception.ShareItException; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.booking.service.handler.owner.OwnerStateProcessor; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ActiveProfiles("test") +@ContextConfiguration(classes = {ShareItApp.class, TestConfig.class}) +@ExtendWith(MockitoExtension.class) +@DisplayName("Booking Service Tests") +class BookingServiceImplTest { + @Mock + private BookingRepository bookingRepository; + @Mock + private UserRepository userRepository; + @Mock + private ItemRepository itemRepository; + @Mock + private BookingMapper bookingMapper; + @Mock + private BookerStateProcessor bookerStateProcessor; + @Mock + private OwnerStateProcessor ownerStateProcessor; + @InjectMocks + private BookingServiceImpl bookingService; + + private User owner; + private User booker; + private Item item; + private Booking booking; + private BookingDto bookingDto; + private LocalDateTime now; + + @BeforeEach + void setUp() { + now = LocalDateTime.now(); + owner = User.builder() + .id(1L) + .name("Owner") + .email("owner@test.com") + .build(); + booker = User.builder() + .id(2L) + .name("Booker") + .email("booker@test.com") + .build(); + item = Item.builder() + .id(1L) + .name("Test Item") + .description("Test Description") + .available(true) + .owner(owner) + .build(); + booking = Booking.builder() + .id(1L) + .start(now.plusDays(1)) + .end(now.plusDays(2)) + .item(item) + .booker(booker) + .status(BookingStatus.WAITING) + .build(); + bookingDto = BookingDto.builder() + .id(1L) + .start(now.plusDays(1)) + .end(now.plusDays(2)) + .itemId(1L) + .item(ItemDto.builder().id(1L).build()) + .booker(UserDto.builder().id(2L).build()) + .status(BookingStatus.WAITING) + .build(); + } + + @Nested // Тесты на создание бронирования + @DisplayName("Create Booking Tests") + class CreateBookingTests { + @Test + @DisplayName("Should create booking successfully") + void createBookingSuccessfully() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(booker)); + when(itemRepository.findById(anyLong())).thenReturn(Optional.of(item)); + when(bookingRepository.save(any(Booking.class))).thenReturn(booking); + when(bookingMapper.toBookingDto(any(Booking.class))).thenReturn(bookingDto); + + BookingDto result = bookingService.create(bookingDto, booker.getId()); + + assertNotNull(result); + assertEquals(bookingDto.getId(), result.getId()); + assertEquals(bookingDto.getStart(), result.getStart()); + assertEquals(bookingDto.getEnd(), result.getEnd()); + assertEquals(bookingDto.getStatus(), result.getStatus()); + verify(bookingRepository).save(any(Booking.class)); + } + + @Test + @DisplayName("Should throw exception when user doesn't exist") + void createBookingWithNonExistentUser() { + when(userRepository.findById(anyLong())).thenReturn(Optional.empty()); + + assertThrows(ShareItException.NotFoundException.class, + () -> bookingService.create(bookingDto, 999L)); + verify(bookingRepository, never()).save(any(Booking.class)); + } + + @Test + @DisplayName("Should throw exception when item doesn't exist") + void createBookingWithNonExistentItem() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(booker)); + when(itemRepository.findById(anyLong())).thenReturn(Optional.empty()); + + assertThrows(ShareItException.NotFoundException.class, + () -> bookingService.create(bookingDto, booker.getId())); + verify(bookingRepository, never()).save(any(Booking.class)); + } + + @Test + @DisplayName("Should throw exception when item is unavailable") + void createBookingWithUnavailableItem() { + item.setAvailable(false); + when(userRepository.findById(anyLong())).thenReturn(Optional.of(booker)); + when(itemRepository.findById(anyLong())).thenReturn(Optional.of(item)); + + assertThrows(ShareItException.BadRequestException.class, + () -> bookingService.create(bookingDto, booker.getId())); + verify(bookingRepository, never()).save(any(Booking.class)); + } + + @Test + @DisplayName("Should throw exception when owner tries to book own item") + void createBookingByOwner() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(owner)); + when(itemRepository.findById(anyLong())).thenReturn(Optional.of(item)); + + assertThrows(ShareItException.NotFoundException.class, + () -> bookingService.create(bookingDto, owner.getId())); + verify(bookingRepository, never()).save(any(Booking.class)); + } + + @Test + @DisplayName("Should throw exception when booking dates are invalid") + void createBookingWithInvalidDates() { + bookingDto.setStart(now.plusDays(2)); + bookingDto.setEnd(now.plusDays(1)); + when(userRepository.findById(anyLong())).thenReturn(Optional.of(booker)); + when(itemRepository.findById(anyLong())).thenReturn(Optional.of(item)); + + assertThrows(ShareItException.BadRequestException.class, + () -> bookingService.create(bookingDto, booker.getId())); + verify(bookingRepository, never()).save(any(Booking.class)); + } + } + + @Nested // Тесты на подтверждение бронирования + @DisplayName("Approve Booking Tests") + class ApproveBookingTests { + @Test + @DisplayName("Should approve booking successfully") + void approveBookingSuccessfully() { + booking.setStatus(BookingStatus.WAITING); + when(bookingRepository.findById(anyLong())).thenReturn(Optional.of(booking)); + Booking approvedBooking = Booking.builder() + .id(booking.getId()) + .start(booking.getStart()) + .end(booking.getEnd()) + .item(booking.getItem()) + .booker(booking.getBooker()) + .status(BookingStatus.APPROVED) + .build(); + when(bookingRepository.save(any(Booking.class))).thenReturn(approvedBooking); + BookingDto approvedBookingDto = BookingDto.builder() + .id(bookingDto.getId()) + .start(bookingDto.getStart()) + .end(bookingDto.getEnd()) + .item(bookingDto.getItem()) + .booker(bookingDto.getBooker()) + .status(BookingStatus.APPROVED) + .build(); + when(bookingMapper.toBookingDto(approvedBooking)).thenReturn(approvedBookingDto); + + BookingDto result = bookingService.approve(booking.getId(), owner.getId(), true); + + assertNotNull(result); + assertEquals(BookingStatus.APPROVED, result.getStatus()); + verify(bookingRepository).save(any(Booking.class)); + } + + @Test + @DisplayName("Should reject booking successfully") + void rejectBookingSuccessfully() { + booking.setStatus(BookingStatus.WAITING); + when(bookingRepository.findById(anyLong())).thenReturn(Optional.of(booking)); + Booking rejectedBooking = Booking.builder() + .id(booking.getId()) + .start(booking.getStart()) + .end(booking.getEnd()) + .item(booking.getItem()) + .booker(booking.getBooker()) + .status(BookingStatus.REJECTED) + .build(); + when(bookingRepository.save(any(Booking.class))).thenReturn(rejectedBooking); + BookingDto rejectedBookingDto = BookingDto.builder() + .id(bookingDto.getId()) + .start(bookingDto.getStart()) + .end(bookingDto.getEnd()) + .item(bookingDto.getItem()) + .booker(bookingDto.getBooker()) + .status(BookingStatus.REJECTED) + .build(); + when(bookingMapper.toBookingDto(rejectedBooking)).thenReturn(rejectedBookingDto); + + BookingDto result = bookingService.approve(booking.getId(), owner.getId(), false); + + assertNotNull(result); + assertEquals(BookingStatus.REJECTED, result.getStatus()); + verify(bookingRepository).save(any(Booking.class)); + } + + @Test + @DisplayName("Should throw exception when booking doesn't exist") + void approveBookingWithNonExistentBooking() { + when(bookingRepository.findById(anyLong())).thenReturn(Optional.empty()); + + assertThrows(ShareItException.NotFoundException.class, + () -> bookingService.approve(999L, owner.getId(), true)); + verify(bookingRepository, never()).save(any(Booking.class)); + } + + @Test + @DisplayName("Should throw exception when non-owner tries to approve") + void approveBookingByNonOwner() { + when(bookingRepository.findById(anyLong())).thenReturn(Optional.of(booking)); + + assertThrows(ShareItException.ForbiddenException.class, + () -> bookingService.approve(booking.getId(), booker.getId(), true)); + verify(bookingRepository, never()).save(any(Booking.class)); + } + + @Test + @DisplayName("Should throw exception when booking already processed") + void approveAlreadyProcessedBooking() { + booking.setStatus(BookingStatus.APPROVED); + when(bookingRepository.findById(anyLong())).thenReturn(Optional.of(booking)); + + assertThrows(ShareItException.BadRequestException.class, + () -> bookingService.approve(booking.getId(), owner.getId(), true)); + verify(bookingRepository, never()).save(any(Booking.class)); + } + } + + @Nested // Тесты на получение бронирования + @DisplayName("Get Booking Tests") + class GetBookingTests { + @Test + @DisplayName("Should get booking by ID successfully") + void getBookingByIdSuccessfully() { + when(bookingRepository.findById(anyLong())).thenReturn(Optional.of(booking)); + when(bookingMapper.toBookingDto(any(Booking.class))).thenReturn(bookingDto); + + BookingDto result = bookingService.getById(booking.getId(), booker.getId()); + + assertNotNull(result); + assertEquals(bookingDto.getId(), result.getId()); + verify(bookingMapper).toBookingDto(booking); + } + + @Test + @DisplayName("Should throw exception when booking doesn't exist") + void getBookingByIdWithNonExistentBooking() { + when(bookingRepository.findById(anyLong())).thenReturn(Optional.empty()); + + assertThrows(ShareItException.NotFoundException.class, + () -> bookingService.getById(999L, booker.getId())); + } + + @Test + @DisplayName("Should throw exception when unauthorized user tries to get booking") + void getBookingByIdByUnauthorizedUser() { + User randomUser = User.builder().id(3L).build(); + when(bookingRepository.findById(anyLong())).thenReturn(Optional.of(booking)); + + assertThrows(ShareItException.NotFoundException.class, + () -> bookingService.getById(booking.getId(), randomUser.getId())); + } + } + + @Nested // Тесты на получение бронирований по статусу + @DisplayName("Get Bookings By Booker Tests") + class GetBookingsByBookerTests { + @Test + @DisplayName("Should get all bookings by booker") + void getAllBookingsByBooker() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(booker)); + when(bookerStateProcessor.process(eq(BookingState.ALL), eq(booker.getId()), any(LocalDateTime.class))) + .thenReturn(Collections.singletonList(booking)); + when(bookingMapper.toBookingDto(any(Booking.class))).thenReturn(bookingDto); + + List result = bookingService.getAllByBooker(booker.getId(), "ALL"); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(bookingDto.getId(), result.get(0).getId()); + verify(bookerStateProcessor).process(eq(BookingState.ALL), eq(booker.getId()), any(LocalDateTime.class)); + } + + @Test + @DisplayName("Should get current bookings by booker") + void getCurrentBookingsByBooker() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(booker)); + when(bookerStateProcessor.process(eq(BookingState.CURRENT), eq(booker.getId()), any(LocalDateTime.class))) + .thenReturn(Collections.singletonList(booking)); + when(bookingMapper.toBookingDto(any(Booking.class))).thenReturn(bookingDto); + + List result = bookingService.getAllByBooker(booker.getId(), "CURRENT"); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(bookerStateProcessor).process(eq(BookingState.CURRENT), eq(booker.getId()), any(LocalDateTime.class)); + } + + @Test + @DisplayName("Should get past bookings by booker") + void getPastBookingsByBooker() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(booker)); + when(bookerStateProcessor.process(eq(BookingState.PAST), eq(booker.getId()), any(LocalDateTime.class))) + .thenReturn(Collections.singletonList(booking)); + when(bookingMapper.toBookingDto(any(Booking.class))).thenReturn(bookingDto); + + List result = bookingService.getAllByBooker(booker.getId(), "PAST"); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(bookerStateProcessor).process(eq(BookingState.PAST), eq(booker.getId()), any(LocalDateTime.class)); + } + + @Test + @DisplayName("Should get future bookings by booker") + void getFutureBookingsByBooker() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(booker)); + when(bookerStateProcessor.process(eq(BookingState.FUTURE), eq(booker.getId()), any(LocalDateTime.class))) + .thenReturn(Collections.singletonList(booking)); + when(bookingMapper.toBookingDto(any(Booking.class))).thenReturn(bookingDto); + + List result = bookingService.getAllByBooker(booker.getId(), "FUTURE"); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(bookerStateProcessor).process(eq(BookingState.FUTURE), eq(booker.getId()), any(LocalDateTime.class)); + } + + @Test + @DisplayName("Should get waiting bookings by booker") + void getWaitingBookingsByBooker() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(booker)); + when(bookerStateProcessor.process(eq(BookingState.WAITING), eq(booker.getId()), any(LocalDateTime.class))) + .thenReturn(Collections.singletonList(booking)); + when(bookingMapper.toBookingDto(any(Booking.class))).thenReturn(bookingDto); + + List result = bookingService.getAllByBooker(booker.getId(), "WAITING"); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(bookerStateProcessor).process(eq(BookingState.WAITING), eq(booker.getId()), any(LocalDateTime.class)); + } + + @Test + @DisplayName("Should get rejected bookings by booker") + void getRejectedBookingsByBooker() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(booker)); + when(bookerStateProcessor.process(eq(BookingState.REJECTED), eq(booker.getId()), any(LocalDateTime.class))) + .thenReturn(Collections.singletonList(booking)); + when(bookingMapper.toBookingDto(any(Booking.class))).thenReturn(bookingDto); + + List result = bookingService.getAllByBooker(booker.getId(), "REJECTED"); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(bookerStateProcessor).process(eq(BookingState.REJECTED), eq(booker.getId()), any(LocalDateTime.class)); + } + + @Test + @DisplayName("Should throw exception when state is invalid") + void getBookingsByBookerWithInvalidState() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(booker)); + + assertThrows(ShareItException.BadRequestException.class, + () -> bookingService.getAllByBooker(booker.getId(), "INVALID_STATE")); + } + } + + @Nested // Тесты для получения бронирований по владельцу + @DisplayName("Get Bookings By Owner Tests") + class GetBookingsByOwnerTests { + @Test + @DisplayName("Should get all bookings by owner") + void getAllBookingsByOwner() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(owner)); + when(ownerStateProcessor.process(eq(BookingState.ALL), eq(owner.getId()), any(LocalDateTime.class))) + .thenReturn(Collections.singletonList(booking)); + when(bookingMapper.toBookingDto(any(Booking.class))).thenReturn(bookingDto); + + List result = bookingService.getAllByOwner(owner.getId(), "ALL"); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(bookingDto.getId(), result.get(0).getId()); + verify(ownerStateProcessor).process(eq(BookingState.ALL), eq(owner.getId()), any(LocalDateTime.class)); + } + + @Test + @DisplayName("Should get current bookings by owner") + void getCurrentBookingsByOwner() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(owner)); + when(ownerStateProcessor.process(eq(BookingState.CURRENT), eq(owner.getId()), any(LocalDateTime.class))) + .thenReturn(Collections.singletonList(booking)); + when(bookingMapper.toBookingDto(any(Booking.class))).thenReturn(bookingDto); + + List result = bookingService.getAllByOwner(owner.getId(), "CURRENT"); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(ownerStateProcessor).process(eq(BookingState.CURRENT), eq(owner.getId()), any(LocalDateTime.class)); + } + + @Test + @DisplayName("Should get past bookings by owner") + void getPastBookingsByOwner() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(owner)); + when(ownerStateProcessor.process(eq(BookingState.PAST), eq(owner.getId()), any(LocalDateTime.class))) + .thenReturn(Collections.singletonList(booking)); + when(bookingMapper.toBookingDto(any(Booking.class))).thenReturn(bookingDto); + + List result = bookingService.getAllByOwner(owner.getId(), "PAST"); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(ownerStateProcessor).process(eq(BookingState.PAST), eq(owner.getId()), any(LocalDateTime.class)); + } + + @Test + @DisplayName("Should get future bookings by owner") + void getFutureBookingsByOwner() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(owner)); + when(ownerStateProcessor.process(eq(BookingState.FUTURE), eq(owner.getId()), any(LocalDateTime.class))) + .thenReturn(Collections.singletonList(booking)); + when(bookingMapper.toBookingDto(any(Booking.class))).thenReturn(bookingDto); + + List result = bookingService.getAllByOwner(owner.getId(), "FUTURE"); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(ownerStateProcessor).process(eq(BookingState.FUTURE), eq(owner.getId()), any(LocalDateTime.class)); + } + + @Test + @DisplayName("Should get waiting bookings by owner") + void getWaitingBookingsByOwner() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(owner)); + when(ownerStateProcessor.process(eq(BookingState.WAITING), eq(owner.getId()), any(LocalDateTime.class))) + .thenReturn(Collections.singletonList(booking)); + when(bookingMapper.toBookingDto(any(Booking.class))).thenReturn(bookingDto); + + List result = bookingService.getAllByOwner(owner.getId(), "WAITING"); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(ownerStateProcessor).process(eq(BookingState.WAITING), eq(owner.getId()), any(LocalDateTime.class)); + } + + @Test + @DisplayName("Should get rejected bookings by owner") + void getRejectedBookingsByOwner() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(owner)); + when(ownerStateProcessor.process(eq(BookingState.REJECTED), eq(owner.getId()), any(LocalDateTime.class))) + .thenReturn(Collections.singletonList(booking)); + when(bookingMapper.toBookingDto(any(Booking.class))).thenReturn(bookingDto); + + List result = bookingService.getAllByOwner(owner.getId(), "REJECTED"); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(ownerStateProcessor).process(eq(BookingState.REJECTED), eq(owner.getId()), any(LocalDateTime.class)); + } + + @Test + @DisplayName("Should throw exception when state is invalid") + void getBookingsByOwnerWithInvalidState() { + when(userRepository.findById(anyLong())).thenReturn(Optional.of(owner)); + + assertThrows(ShareItException.BadRequestException.class, + () -> bookingService.getAllByOwner(owner.getId(), "INVALID_STATE")); + } + } + + @Nested // Тесты для получения бронирований пользователя + @DisplayName("User Validation Tests") + class UserValidationTests { + @Test + @DisplayName("Should throw exception when user doesn't exist") + void getBookingsByNonExistentUser() { + when(userRepository.findById(anyLong())).thenReturn(Optional.empty()); + + assertThrows(ShareItException.NotFoundException.class, + () -> bookingService.getAllByBooker(999L, "ALL")); + assertThrows(ShareItException.NotFoundException.class, + () -> bookingService.getAllByOwner(999L, "ALL")); + } + } +} \ No newline at end of file diff --git a/src/test/java/ru/practicum/shareit/item/controller/CommentControllerTest.java b/src/test/java/ru/practicum/shareit/item/controller/CommentControllerTest.java new file mode 100644 index 0000000..d7e84b3 --- /dev/null +++ b/src/test/java/ru/practicum/shareit/item/controller/CommentControllerTest.java @@ -0,0 +1,115 @@ +package ru.practicum.shareit.item.controller; + +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.shareit.ShareItApp; +import ru.practicum.shareit.TestConfig; +import ru.practicum.shareit.booking.controller.BookingController; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.exception.ShareItException; +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.user.UserMapper; +import ru.practicum.shareit.user.controller.UserController; +import ru.practicum.shareit.user.dto.UserDto; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ActiveProfiles("test") +@ContextConfiguration(classes = {ShareItApp.class, TestConfig.class}) +@SpringBootTest +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +class CommentControllerTest { + + @Autowired + private ItemController itemController; + + @Autowired + private UserController userController; + + @Autowired + private BookingController bookingController; + + @Autowired + private UserMapper userMapper; + + private UserDto ownerDto; + private UserDto bookerDto; + private ItemDto itemDto; + private BookingDto bookingDto; + + @BeforeEach + void setUp() { + String ownerEmail = "owner" + System.currentTimeMillis() + "@email.com"; + ownerDto = UserDto.builder() + .name("Item Owner") + .email(ownerEmail) + .build(); + ownerDto = userController.create(userMapper.toUser(ownerDto)); + + String bookerEmail = "booker" + System.currentTimeMillis() + "@email.com"; + bookerDto = UserDto.builder() + .name("Booker User") + .email(bookerEmail) + .build(); + bookerDto = userController.create(userMapper.toUser(bookerDto)); + + itemDto = ItemDto.builder() + .name("Test Item") + .description("Test Description") + .available(true) + .build(); + itemDto = itemController.create(ownerDto.getId(), itemDto); + + LocalDateTime start = LocalDateTime.now().plusHours(1); + LocalDateTime end = LocalDateTime.now().plusHours(2); + bookingDto = BookingDto.builder() + .itemId(itemDto.getId()) + .start(start) + .end(end) + .build(); + } + + @Test // Тест на успешное добавление комментария без бронирования + @DisplayName("Error when adding comment without booking") + void errorWhenAddingCommentWithoutBooking() { + CommentDto commentDto = CommentDto.builder() + .text("I haven't rented this item") + .build(); + + assertThrows(ShareItException.BadRequestException.class, + () -> itemController.createComment(itemDto.getId(), commentDto, bookerDto.getId())); + } + + @Test // Тест на успешное добавление комментария с бронированием + @Transactional + @DisplayName("Error when adding comment with future booking") + void errorWhenAddingCommentWithFutureBooking() { + LocalDateTime start = LocalDateTime.now().plusDays(1); + LocalDateTime end = LocalDateTime.now().plusDays(2); + BookingDto futureBooking = BookingDto.builder() + .itemId(itemDto.getId()) + .start(start) + .end(end) + .build(); + + BookingDto createdBooking = bookingController.createBooking(futureBooking, bookerDto.getId()).getBody(); + bookingController.approve(createdBooking.getId(), ownerDto.getId(), true).getBody(); + + CommentDto commentDto = CommentDto.builder() + .text("I haven't used this item yet") + .build(); + + assertThrows(ShareItException.BadRequestException.class, + () -> itemController.createComment(itemDto.getId(), commentDto, bookerDto.getId())); + } +} \ No newline at end of file diff --git a/src/test/java/ru/practicum/shareit/item/controller/ItemBookingInfoTest.java b/src/test/java/ru/practicum/shareit/item/controller/ItemBookingInfoTest.java new file mode 100644 index 0000000..b5c1e9e --- /dev/null +++ b/src/test/java/ru/practicum/shareit/item/controller/ItemBookingInfoTest.java @@ -0,0 +1,192 @@ +package ru.practicum.shareit.item.controller; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.ShareItApp; +import ru.practicum.shareit.TestConfig; +import ru.practicum.shareit.booking.controller.BookingController; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.dto.BookingShortDto; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.user.UserMapper; +import ru.practicum.shareit.user.controller.UserController; +import ru.practicum.shareit.user.dto.UserDto; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ActiveProfiles("test") +@ContextConfiguration(classes = {ShareItApp.class, TestConfig.class}) +@SpringBootTest +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +class ItemBookingInfoTest { + + @Autowired + private ItemController itemController; + + @Autowired + private UserController userController; + + @Autowired + private BookingController bookingController; + + @Autowired + private UserMapper userMapper; + + private UserDto ownerDto; + private UserDto bookerDto; + private ItemDto itemDto; + + @BeforeEach + void setUp() { + String ownerEmail = "owner" + System.currentTimeMillis() + "@email.com"; + ownerDto = UserDto.builder() + .name("Item Owner") + .email(ownerEmail) + .build(); + ownerDto = userController.create(userMapper.toUser(ownerDto)); + + String bookerEmail = "booker" + System.currentTimeMillis() + "@email.com"; + bookerDto = UserDto.builder() + .name("Booker User") + .email(bookerEmail) + .build(); + bookerDto = userController.create(userMapper.toUser(bookerDto)); + + itemDto = ItemDto.builder() + .name("Test Item") + .description("Test Description") + .available(true) + .build(); + itemDto = itemController.create(ownerDto.getId(), itemDto); + } + + @Test // Тесты на получение бронирований для вещи владельца + @Transactional + @DisplayName("Item should show booking info for owner") + void itemShouldShowBookingInfoForOwner() { + LocalDateTime nearFutureStart = LocalDateTime.now().plusHours(1); + LocalDateTime nearFutureEnd = LocalDateTime.now().plusHours(2); + BookingDto nearFutureBooking = BookingDto.builder() + .itemId(itemDto.getId()) + .start(nearFutureStart) + .end(nearFutureEnd) + .build(); + BookingDto createdNearFutureBooking = bookingController.createBooking(nearFutureBooking, bookerDto.getId()).getBody(); + bookingController.approve(createdNearFutureBooking.getId(), ownerDto.getId(), true).getBody(); // Добавлен .getBody() + + LocalDateTime farFutureStart = LocalDateTime.now().plusDays(1); + LocalDateTime farFutureEnd = LocalDateTime.now().plusDays(2); + BookingDto farFutureBooking = BookingDto.builder() + .itemId(itemDto.getId()) + .start(farFutureStart) + .end(farFutureEnd) + .build(); + BookingDto createdFarFutureBooking = bookingController.createBooking(farFutureBooking, bookerDto.getId()).getBody(); + bookingController.approve(createdFarFutureBooking.getId(), ownerDto.getId(), true).getBody(); // Добавлен .getBody() + + ItemDto itemWithBookings = itemController.getById(itemDto.getId(), ownerDto.getId()); + + assertNotNull(itemWithBookings.getNextBooking()); + + BookingShortDto nextBooking = itemWithBookings.getNextBooking(); + assertNotNull(nextBooking.getId()); + assertEquals(bookerDto.getId(), nextBooking.getBookerId()); + + boolean isNearFutureBooking = nextBooking.getStart().equals(nearFutureStart) && + nextBooking.getEnd().equals(nearFutureEnd); + boolean isFarFutureBooking = nextBooking.getStart().equals(farFutureStart) && + nextBooking.getEnd().equals(farFutureEnd); + + assertTrue(isNearFutureBooking || isFarFutureBooking, + "Next booking should match either near future or far future booking"); + } + + @Test // Тесты на получение бронирований для вещей кроме вещей владельца + @Transactional + @DisplayName("Item should not show booking info for non-owner") + void itemShouldNotShowBookingInfoForNonOwner() { + LocalDateTime start = LocalDateTime.now().plusDays(1); + LocalDateTime end = LocalDateTime.now().plusDays(2); + BookingDto bookingDto = BookingDto.builder() + .itemId(itemDto.getId()) + .start(start) + .end(end) + .build(); + bookingController.createBooking(bookingDto, bookerDto.getId()).getBody(); + + ItemDto itemForNonOwner = itemController.getById(itemDto.getId(), bookerDto.getId()); + + assertNull(itemForNonOwner.getLastBooking()); + assertNull(itemForNonOwner.getNextBooking()); + } + + @Test // Тесты на получение бронирований для всех вещей владельца + @Transactional + @DisplayName("All owner's items should show booking info") + void allOwnerItemsShouldShowBookingInfo() { + ItemDto secondItem = ItemDto.builder() + .name("Second Item") + .description("Another Description") + .available(true) + .build(); + secondItem = itemController.create(ownerDto.getId(), secondItem); + + LocalDateTime start1 = LocalDateTime.now().plusDays(1); + LocalDateTime end1 = LocalDateTime.now().plusDays(2); + BookingDto booking1 = BookingDto.builder() + .itemId(itemDto.getId()) + .start(start1) + .end(end1) + .build(); + BookingDto createdBooking1 = bookingController.createBooking(booking1, bookerDto.getId()).getBody(); + bookingController.approve(createdBooking1.getId(), ownerDto.getId(), true).getBody(); // Добавлен .getBody() + + LocalDateTime start2 = LocalDateTime.now().plusDays(3); + LocalDateTime end2 = LocalDateTime.now().plusDays(4); + BookingDto booking2 = BookingDto.builder() + .itemId(secondItem.getId()) + .start(start2) + .end(end2) + .build(); + BookingDto createdBooking2 = bookingController.createBooking(booking2, bookerDto.getId()).getBody(); + bookingController.approve(createdBooking2.getId(), ownerDto.getId(), true).getBody(); // Добавлен .getBody() + + List ownerItems = itemController.getAll(ownerDto.getId()); + + assertEquals(2, ownerItems.size()); + + ItemDto firstItemResult = ownerItems.stream() + .filter(item -> item.getId().equals(itemDto.getId())) + .findFirst() + .orElseThrow(); + + ItemDto finalSecondItem = secondItem; + ItemDto secondItemResult = ownerItems.stream() + .filter(item -> item.getId().equals(finalSecondItem.getId())) + .findFirst() + .orElseThrow(); + + assertNotNull(firstItemResult.getNextBooking()); + assertEquals(bookerDto.getId(), firstItemResult.getNextBooking().getBookerId()); + assertEquals(start1, firstItemResult.getNextBooking().getStart()); + assertEquals(end1, firstItemResult.getNextBooking().getEnd()); + + assertNotNull(secondItemResult.getNextBooking()); + assertEquals(bookerDto.getId(), secondItemResult.getNextBooking().getBookerId()); + assertEquals(start2, secondItemResult.getNextBooking().getStart()); + assertEquals(end2, secondItemResult.getNextBooking().getEnd()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/practicum/shareit/item/controller/ItemControllerTest.java b/src/test/java/ru/practicum/shareit/item/controller/ItemControllerTest.java index ead826a..dc8cc1c 100644 --- a/src/test/java/ru/practicum/shareit/item/controller/ItemControllerTest.java +++ b/src/test/java/ru/practicum/shareit/item/controller/ItemControllerTest.java @@ -4,47 +4,51 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.DirtiesContext; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.shareit.ShareItApp; +import ru.practicum.shareit.TestConfig; import ru.practicum.shareit.booking.exception.ShareItException; +import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; -import ru.practicum.shareit.user.UserMapper; -import ru.practicum.shareit.user.controller.UserController; -import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.item.service.ItemService; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; - -@SpringBootTest -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +@ActiveProfiles("test") +@ContextConfiguration(classes = {ShareItApp.class, TestConfig.class}) +@ExtendWith(MockitoExtension.class) class ItemControllerTest { - @Autowired - private ItemController itemController; - - @Autowired - private UserController userController; + @Mock + private ItemService itemService; - @Autowired - private UserMapper userMapper; + @InjectMocks + private ItemController itemController; private ItemDto itemDto; - private UserDto userDto; private Long userId; @BeforeEach void setUp() { - userDto = UserDto.builder() - .name("Test Owner") - .email("owner@email.com") - .build(); - UserDto createdUser = userController.create(userMapper.toUser(userDto)); - userId = createdUser.getId(); - + userId = 1L; itemDto = ItemDto.builder() + .id(1L) .name("Test Item") .description("Test Description") .available(true) @@ -57,10 +61,12 @@ class CreateItemTests { @Test @DisplayName("Successful creation of an item") void createItemTest() { + when(itemService.create(any(ItemDto.class), anyLong())).thenReturn(itemDto); + ItemDto createdItem = itemController.create(userId, itemDto); assertNotNull(createdItem); - assertEquals(1L, createdItem.getId()); + assertEquals(itemDto.getId(), createdItem.getId()); assertEquals(itemDto.getName(), createdItem.getName()); assertEquals(itemDto.getDescription(), createdItem.getDescription()); assertEquals(itemDto.getAvailable(), createdItem.getAvailable()); @@ -69,6 +75,9 @@ void createItemTest() { @Test @DisplayName("Error when creating an item with a non-existent user") void createItemWithNonExistentUserTest() { + when(itemService.create(any(ItemDto.class), eq(999L))) + .thenThrow(new ShareItException.NotFoundException("User not found")); + assertThrows(ShareItException.NotFoundException.class, () -> itemController.create(999L, itemDto)); } @@ -80,40 +89,45 @@ class GetItemTests { @Test @DisplayName("Getting an item by ID") void getItemByIdTest() { - ItemDto createdItem = itemController.create(userId, itemDto); - ItemDto retrievedItem = itemController.getById(createdItem.getId()); + when(itemService.getById(anyLong(), anyLong())).thenReturn(itemDto); + + ItemDto retrievedItem = itemController.getById(1L, userId); assertNotNull(retrievedItem); - assertEquals(createdItem.getId(), retrievedItem.getId()); - assertEquals(createdItem.getName(), retrievedItem.getName()); - assertEquals(createdItem.getDescription(), retrievedItem.getDescription()); - assertEquals(createdItem.getAvailable(), retrievedItem.getAvailable()); + assertEquals(itemDto.getId(), retrievedItem.getId()); + assertEquals(itemDto.getName(), retrievedItem.getName()); + assertEquals(itemDto.getDescription(), retrievedItem.getDescription()); + assertEquals(itemDto.getAvailable(), retrievedItem.getAvailable()); } @Test @DisplayName("Getting all the user's items") void getAllUserItemsTest() { - itemController.create(userId, itemDto); - ItemDto secondItem = ItemDto.builder() + .id(2L) .name("Second Item") .description("Another Description") .available(true) .build(); - itemController.create(userId, secondItem); + List items = Arrays.asList(itemDto, secondItem); + + when(itemService.getAll(anyLong())).thenReturn(items); - List items = itemController.getAll(userId); + List retrievedItems = itemController.getAll(userId); - assertEquals(2, items.size()); - assertEquals("Test Item", items.get(0).getName()); - assertEquals("Second Item", items.get(1).getName()); + assertEquals(2, retrievedItems.size()); + assertEquals("Test Item", retrievedItems.get(0).getName()); + assertEquals("Second Item", retrievedItems.get(1).getName()); } @Test @DisplayName("Error when receiving a non-existent item") void getNonExistentItemTest() { + when(itemService.getById(eq(999L), anyLong())) + .thenThrow(new ShareItException.NotFoundException("Item not found")); + assertThrows(ShareItException.NotFoundException.class, - () -> itemController.getById(999L)); + () -> itemController.getById(999L, userId)); } } @@ -123,21 +137,29 @@ class UpdateItemTests { @Test @DisplayName("Full item update") void updateItemTest() { - ItemDto createdItem = itemController.create(userId, itemDto); - ItemDto updateRequest = ItemDto.builder() .name("Updated Item") .description("Updated Description") .available(false) .build(); - ItemDto updatedItem = itemController.update(updateRequest, createdItem.getId(), userId); + ItemDto updatedItem = ItemDto.builder() + .id(1L) + .name("Updated Item") + .description("Updated Description") + .available(false) + .build(); - assertEquals("Updated Item", updatedItem.getName()); - assertEquals("Updated Description", updatedItem.getDescription()); - assertEquals(false, updatedItem.getAvailable()); + when(itemService.update(any(ItemDto.class), anyLong(), anyLong())).thenReturn(updatedItem); + when(itemService.getById(anyLong(), anyLong())).thenReturn(updatedItem); - ItemDto retrievedItem = itemController.getById(createdItem.getId()); + ItemDto result = itemController.update(updateRequest, 1L, userId); + + assertEquals("Updated Item", result.getName()); + assertEquals("Updated Description", result.getDescription()); + assertEquals(false, result.getAvailable()); + + ItemDto retrievedItem = itemController.getById(1L, userId); assertEquals("Updated Item", retrievedItem.getName()); assertEquals("Updated Description", retrievedItem.getDescription()); assertEquals(false, retrievedItem.getAvailable()); @@ -146,32 +168,47 @@ void updateItemTest() { @Test @DisplayName("Partial item update") void partialUpdateItemTest() { - ItemDto createdItem = itemController.create(userId, itemDto); - ItemDto result; + ItemDto nameUpdateRequest = ItemDto.builder().name("New Name").build(); + ItemDto nameUpdatedItem = ItemDto.builder() + .id(1L) + .name("New Name") + .description("Test Description") + .available(true) + .build(); + + when(itemService.update(eq(nameUpdateRequest), anyLong(), anyLong())).thenReturn(nameUpdatedItem); - result = itemController.update( - ItemDto.builder().name("New Name").build(), - createdItem.getId(), - userId - ); + ItemDto result = itemController.update(nameUpdateRequest, 1L, userId); assertEquals("New Name", result.getName()); - assertEquals(itemDto.getDescription(), result.getDescription()); - assertEquals(itemDto.getAvailable(), result.getAvailable()); - - result = itemController.update( - ItemDto.builder().description("New Description").build(), - createdItem.getId(), - userId - ); + assertEquals("Test Description", result.getDescription()); + assertEquals(true, result.getAvailable()); + + ItemDto descUpdateRequest = ItemDto.builder().description("New Description").build(); + ItemDto descUpdatedItem = ItemDto.builder() + .id(1L) + .name("New Name") + .description("New Description") + .available(true) + .build(); + + when(itemService.update(eq(descUpdateRequest), anyLong(), anyLong())).thenReturn(descUpdatedItem); + + result = itemController.update(descUpdateRequest, 1L, userId); assertEquals("New Name", result.getName()); assertEquals("New Description", result.getDescription()); - assertEquals(itemDto.getAvailable(), result.getAvailable()); + assertEquals(true, result.getAvailable()); + + ItemDto availUpdateRequest = ItemDto.builder().available(false).build(); + ItemDto availUpdatedItem = ItemDto.builder() + .id(1L) + .name("New Name") + .description("New Description") + .available(false) + .build(); - result = itemController.update( - ItemDto.builder().available(false).build(), - createdItem.getId(), - userId - ); + when(itemService.update(eq(availUpdateRequest), anyLong(), anyLong())).thenReturn(availUpdatedItem); + + result = itemController.update(availUpdateRequest, 1L, userId); assertEquals("New Name", result.getName()); assertEquals("New Description", result.getDescription()); assertEquals(false, result.getAvailable()); @@ -180,20 +217,15 @@ void partialUpdateItemTest() { @Test @DisplayName("Error when updating an item by a non-owner") void updateItemByNonOwnerTest() { - ItemDto createdItem = itemController.create(userId, itemDto); - - UserDto anotherUserDto = UserDto.builder() - .name("Another User") - .email("another@email.com") - .build(); - UserDto anotherUser = userController.create(userMapper.toUser(anotherUserDto)); - ItemDto updateRequest = ItemDto.builder() .name("Unauthorized Update") .build(); + when(itemService.update(any(ItemDto.class), anyLong(), eq(2L))) + .thenThrow(new ShareItException.NotFoundException("Item not found for this user")); + assertThrows(ShareItException.NotFoundException.class, - () -> itemController.update(updateRequest, createdItem.getId(), anotherUser.getId())); + () -> itemController.update(updateRequest, 1L, 2L)); } } @@ -203,10 +235,15 @@ class DeleteItemTests { @Test @DisplayName("Successful removal of an item") void deleteItemTest() { - ItemDto createdItem = itemController.create(userId, itemDto); + when(itemService.getAll(userId)) + .thenReturn(Collections.singletonList(itemDto)) + .thenReturn(Collections.emptyList()); // для второго вызова (после удаления) + + doNothing().when(itemService).delete(anyLong()); + assertEquals(1, itemController.getAll(userId).size()); - itemController.delete(createdItem.getId()); + itemController.delete(1L); assertEquals(0, itemController.getAll(userId).size()); } @@ -218,32 +255,44 @@ class SearchItemTests { @Test @DisplayName("Search for items based on various criteria") void searchItemsTest() { - itemController.create(userId, itemDto); + ItemDto testItem = ItemDto.builder() + .id(1L) + .name("Test Item") + .description("Test Description") + .available(true) + .build(); - ItemDto secondItem = ItemDto.builder() + ItemDto specialItem = ItemDto.builder() + .id(2L) .name("Special Item") .description("Unique features") .available(true) .build(); - itemController.create(userId, secondItem); - ItemDto unavailableItem = ItemDto.builder() - .name("Unavailable Special") - .description("Not for rent") - .available(false) - .build(); - itemController.create(userId, unavailableItem); + when(itemService.search("Test")) + .thenReturn(Collections.singletonList(testItem)); + + when(itemService.search("Unique")) + .thenReturn(Collections.singletonList(specialItem)); + + when(itemService.search("Item")) + .thenReturn(Arrays.asList(testItem, specialItem)); + + when(itemService.search("Unavailable")) + .thenReturn(Collections.emptyList()); + + when(itemService.search("")) + .thenReturn(Collections.emptyList()); List results = itemController.search("Test"); - assertEquals(1, results.size()); - assertEquals("Test Item", results.get(0).getName()); + assertTrue(results.stream().anyMatch(item -> item.getName().equals("Test Item"))); results = itemController.search("Unique"); - assertEquals(1, results.size()); - assertEquals("Special Item", results.get(0).getName()); + assertTrue(results.stream().anyMatch(item -> item.getName().equals("Special Item"))); results = itemController.search("Item"); - assertEquals(2, results.size()); + assertTrue(results.stream().anyMatch(item -> item.getName().equals("Test Item"))); + assertTrue(results.stream().anyMatch(item -> item.getName().equals("Special Item"))); results = itemController.search("Unavailable"); assertEquals(0, results.size()); @@ -252,4 +301,60 @@ void searchItemsTest() { assertEquals(0, results.size()); } } + + @Nested // Тесты на комментарии + @DisplayName("Comment Tests") + class CommentTests { + @Test + @DisplayName("Create comment successfully") + void createCommentTest() { + CommentDto commentDto = CommentDto.builder() + .text("Great item!") + .build(); + + CommentDto createdComment = CommentDto.builder() + .id(1L) + .text("Great item!") + .authorName("User") + .build(); + + when(itemService.createComment(anyLong(), any(CommentDto.class), anyLong())) + .thenReturn(createdComment); + + CommentDto result = itemController.createComment(1L, commentDto, userId); + + assertNotNull(result); + assertEquals(1L, result.getId()); + assertEquals("Great item!", result.getText()); + assertEquals("User", result.getAuthorName()); + } + + @Test + @DisplayName("Error when creating comment for non-existent item") + void createCommentForNonExistentItemTest() { + CommentDto commentDto = CommentDto.builder() + .text("Great item!") + .build(); + + when(itemService.createComment(eq(999L), any(CommentDto.class), anyLong())) + .thenThrow(new ShareItException.NotFoundException("Item not found")); + + assertThrows(ShareItException.NotFoundException.class, + () -> itemController.createComment(999L, commentDto, userId)); + } + + @Test + @DisplayName("Error when user hasn't booked the item") + void createCommentWithoutBookingTest() { + CommentDto commentDto = CommentDto.builder() + .text("Great item!") + .build(); + + when(itemService.createComment(anyLong(), any(CommentDto.class), anyLong())) + .thenThrow(new ShareItException.BadRequestException("User hasn't booked this item")); + + assertThrows(ShareItException.BadRequestException.class, + () -> itemController.createComment(1L, commentDto, userId)); + } + } } \ No newline at end of file diff --git a/src/test/java/ru/practicum/shareit/user/controller/UserControllerTest.java b/src/test/java/ru/practicum/shareit/user/controller/UserControllerTest.java index c1c5962..3254097 100644 --- a/src/test/java/ru/practicum/shareit/user/controller/UserControllerTest.java +++ b/src/test/java/ru/practicum/shareit/user/controller/UserControllerTest.java @@ -7,6 +7,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.shareit.ShareItApp; +import ru.practicum.shareit.TestConfig; import ru.practicum.shareit.booking.exception.ShareItException; import ru.practicum.shareit.user.UserMapper; import ru.practicum.shareit.user.dto.UserDto; @@ -17,7 +21,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +@ActiveProfiles("test") +@ContextConfiguration(classes = {ShareItApp.class, TestConfig.class}) @SpringBootTest @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) class UserControllerTest { @@ -32,11 +39,12 @@ class UserControllerTest { @BeforeEach void setUp() { + String uniqueEmail = "test" + System.currentTimeMillis() + "@email.com"; + userDto = UserDto.builder() .name("Test User") - .email("test@email.com") + .email(uniqueEmail) .build(); - user = userMapper.toUser(userDto); } @@ -49,7 +57,7 @@ void createUserTest() { UserDto createdUser = userController.create(user); assertNotNull(createdUser); - assertEquals(1L, createdUser.getId()); + assertNotNull(createdUser.getId()); // Проверяем, что ID не null, а не конкретное значение assertEquals(userDto.getName(), createdUser.getName()); assertEquals(userDto.getEmail(), createdUser.getEmail()); } @@ -61,7 +69,7 @@ void createUserWithDuplicateEmailTest() { User duplicateUser = User.builder() .name("Another User") - .email("test@email.com") // Тот же email + .email(user.getEmail()) // Тот же email .build(); assertThrows(ShareItException.ConflictException.class, @@ -87,27 +95,35 @@ void getUserByIdTest() { @Test @DisplayName("Getting all users") void getAllUsersTest() { - userController.create(user); + UserDto createdUser = userController.create(user); - // Создаем второго пользователя + String secondEmail = "second" + System.currentTimeMillis() + "@email.com"; UserDto secondUser = UserDto.builder() .name("Second User") - .email("second@email.com") + .email(secondEmail) .build(); - userController.create(userMapper.toUser(secondUser)); + UserDto createdSecondUser = userController.create(userMapper.toUser(secondUser)); List users = userController.getAll(); - assertEquals(2, users.size()); - assertEquals("Test User", users.get(0).getName()); - assertEquals("Second User", users.get(1).getName()); + assertTrue(users.size() >= 2); + assertTrue(users.stream().anyMatch(u -> u.getId().equals(createdUser.getId()))); + assertTrue(users.stream().anyMatch(u -> u.getId().equals(createdSecondUser.getId()))); } @Test @DisplayName("Error when receiving a non-existent user") void getNonExistentUserTest() { + List allUsers = userController.getAll(); + long maxId = allUsers.stream() + .mapToLong(UserDto::getId) + .max() + .orElse(0); + + long nonExistentId = maxId + 1000; + assertThrows(ShareItException.NotFoundException.class, - () -> userController.getById(999L)); + () -> userController.getById(nonExistentId)); } } @@ -119,19 +135,20 @@ class UpdateUserTests { void updateUserTest() { UserDto createdUser = userController.create(user); + String updatedEmail = "updated" + System.currentTimeMillis() + "@email.com"; User updatedUser = User.builder() .name("Updated Name") - .email("updated@email.com") + .email(updatedEmail) .build(); UserDto result = userController.update(updatedUser, createdUser.getId()); assertEquals("Updated Name", result.getName()); - assertEquals("updated@email.com", result.getEmail()); + assertEquals(updatedEmail, result.getEmail()); UserDto retrievedUser = userController.getById(createdUser.getId()); assertEquals("Updated Name", retrievedUser.getName()); - assertEquals("updated@email.com", retrievedUser.getEmail()); + assertEquals(updatedEmail, retrievedUser.getEmail()); } @Test // Тест на частичное обновление пользователя @@ -147,12 +164,13 @@ void partialUpdateUserTest() { assertEquals("New Name", result.getName()); assertEquals(userDto.getEmail(), result.getEmail()); + String newEmail = "new" + System.currentTimeMillis() + "@email.com"; User emailUpdate = User.builder() - .email("new@email.com") + .email(newEmail) .build(); result = userController.update(emailUpdate, createdUser.getId()); assertEquals("New Name", result.getName()); - assertEquals("new@email.com", result.getEmail()); + assertEquals(newEmail, result.getEmail()); } } @@ -163,11 +181,12 @@ class DeleteUserTests { @DisplayName("Successful user deletion") void deleteUserTest() { UserDto createdUser = userController.create(user); - assertEquals(1, userController.getAll().size()); + int initialSize = userController.getAll().size(); userController.delete(createdUser.getId()); - assertEquals(0, userController.getAll().size()); + int newSize = userController.getAll().size(); + assertEquals(initialSize - 1, newSize); assertThrows(ShareItException.NotFoundException.class, () -> userController.getById(createdUser.getId()));