diff --git a/.run/ShareItGateway.run.xml b/.run/ShareItGateway.run.xml new file mode 100644 index 0000000..32c8129 --- /dev/null +++ b/.run/ShareItGateway.run.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/.run/ShareItServer.run.xml b/.run/ShareItServer.run.xml new file mode 100644 index 0000000..a8ed9e5 --- /dev/null +++ b/.run/ShareItServer.run.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ffc6460 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +services: + gateway: + build: gateway + image: shareit-gateway + container_name: shareit-gateway + ports: + - "8080:8080" + depends_on: + - server + environment: + - SHAREIT_SERVER_URL=http://server:9090 + + server: + build: server + image: shareit-server + container_name: shareit-server + ports: + - "9090:9090" + depends_on: + - db + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/shareit + - SPRING_DATASOURCE_USERNAME=dbuser + - SPRING_DATASOURCE_PASSWORD=12345 + + db: + image: postgres:16.1 + container_name: postgres + ports: + - "6541:5432" + environment: + - POSTGRES_PASSWORD=12345 + - POSTGRES_USER=dbuser + - POSTGRES_DB=shareit + healthcheck: + test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER + timeout: 5s + interval: 5s + retries: 10 \ No newline at end of file diff --git a/gateway/Dockerfile b/gateway/Dockerfile new file mode 100644 index 0000000..0ff1817 --- /dev/null +++ b/gateway/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:21-jre-jammy +VOLUME /tmp +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file diff --git a/gateway/pom.xml b/gateway/pom.xml new file mode 100644 index 0000000..f3394c1 --- /dev/null +++ b/gateway/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + ru.practicum + shareit + 0.0.1-SNAPSHOT + + + shareit-gateway + 0.0.1-SNAPSHOT + + ShareIt Gateway + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.hibernate.validator + hibernate-validator + + + + org.apache.httpcomponents.client5 + httpclient5 + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/ShareItGateway.java b/gateway/src/main/java/ru/practicum/shareit/ShareItGateway.java new file mode 100644 index 0000000..4cbc16c --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/ShareItGateway.java @@ -0,0 +1,12 @@ +package ru.practicum.shareit; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ShareItGateway { + public static void main(String[] args) { + SpringApplication.run(ShareItGateway.class, args); + } + +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java b/gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java new file mode 100644 index 0000000..7fb5e1f --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/BookingClient.java @@ -0,0 +1,53 @@ +package ru.practicum.shareit.booking; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.util.DefaultUriBuilderFactory; +import ru.practicum.shareit.booking.dto.BookingDtoRequestCreate; +import ru.practicum.shareit.client.BaseClient; +import ru.practicum.shareit.booking.dto.BookingState; + +import java.util.Map; + +@Service +public class BookingClient extends BaseClient { + @Autowired + public BookingClient(@Value("http://shareit-server:9090") String serverUrl, RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + "/bookings")) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + } + + public ResponseEntity create(Long userId, BookingDtoRequestCreate booking) { + return post("", userId, booking); + } + + public ResponseEntity approve(Long userId, Long bookingId, Boolean approved) { + return patch("/" + bookingId + "?approved=" + approved, userId); + } + + public ResponseEntity get(Long userId, Long bookingId) { + return get("/" + bookingId, userId); + } + + public ResponseEntity getAllBookingCurrentUser(Long userId, BookingState state) { + Map parameters = Map.of( + "state", state.name() + ); + return get("?state={state}", userId, parameters); + } + + public ResponseEntity getAllBookingAllItemCurrentUser(Long userId, BookingState state) { + Map parameters = Map.of( + "state", state.name() + ); + return get("/owner?state={state}", userId, parameters); + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java b/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java new file mode 100644 index 0000000..3609343 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -0,0 +1,67 @@ +package ru.practicum.shareit.booking; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.booking.dto.BookingDtoRequestCreate; +import ru.practicum.shareit.booking.dto.BookingState; + +@Controller +@RequestMapping(path = "/bookings") +@RequiredArgsConstructor +@Validated +public class BookingController { + private final BookingClient bookingClient; + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public ResponseEntity create( + @RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId, + @RequestBody @Valid BookingDtoRequestCreate booking + ) { + return bookingClient.create(userId, booking); + } + + @ResponseStatus(HttpStatus.OK) + @PatchMapping("/{bookingId}") + public ResponseEntity approve( + @RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId, + @PathVariable @Positive Long bookingId, + @RequestParam("approved") Boolean approved + ) { + return bookingClient.approve(userId, bookingId, approved); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/{bookingId}") + public ResponseEntity get( + @RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId, + @PathVariable @Positive Long bookingId + ) { + return bookingClient.get(userId, bookingId); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping + public ResponseEntity getAllBookingCurrentUser( + @RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId, + @RequestParam(name = "state", defaultValue = "ALL") BookingState state + ) { + return bookingClient.getAllBookingCurrentUser(userId, state); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/owner") + public ResponseEntity getAllBookingAllItemCurrentUser( + @RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId, + @RequestParam(name = "state", defaultValue = "ALL") BookingState state + ) { + return bookingClient.getAllBookingAllItemCurrentUser(userId, state); + } + +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookItemRequestDto.java b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookItemRequestDto.java new file mode 100644 index 0000000..738f5c0 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookItemRequestDto.java @@ -0,0 +1,20 @@ +package ru.practicum.shareit.booking.dto; + +import java.time.LocalDateTime; + +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.FutureOrPresent; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class BookItemRequestDto { + private long itemId; + @FutureOrPresent + private LocalDateTime start; + @Future + private LocalDateTime end; +} diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingDtoRequestCreate.java b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingDtoRequestCreate.java new file mode 100644 index 0000000..a7d9f73 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingDtoRequestCreate.java @@ -0,0 +1,20 @@ +package ru.practicum.shareit.booking.dto; + +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +import java.time.LocalDateTime; + +public record BookingDtoRequestCreate( + @NotNull(message = "Необходимо указать идентификатор вещи") + @Positive(message = "Идентификатор вещи должен быть больше нуля") + Long itemId, + @NotNull(message = "Необходимо указать дату и время начала аренды") + @FutureOrPresent(message = "Нельзя указывать прошедшую дату и время начала аренды") + LocalDateTime start, + @FutureOrPresent(message = "Нельзя указывать прошедшую дату и время окончания аренды") + @NotNull(message = "Необходимо указать дату и время окончания аренды") + LocalDateTime end +) { +} diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingDtoResponse.java b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingDtoResponse.java new file mode 100644 index 0000000..4ffa515 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingDtoResponse.java @@ -0,0 +1,16 @@ +package ru.practicum.shareit.booking.dto; + +import ru.practicum.shareit.item.dto.ItemDtoResponse; +import ru.practicum.shareit.user.dto.UserDtoResponse; + +import java.time.LocalDateTime; + +public record BookingDtoResponse( + Long id, + LocalDateTime start, + LocalDateTime end, + Status status, + ItemDtoResponse item, + UserDtoResponse booker +) { +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingState.java b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingState.java new file mode 100644 index 0000000..f98fb8f --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingState.java @@ -0,0 +1,21 @@ +package ru.practicum.shareit.booking.dto; + +import java.util.Optional; + +public enum BookingState { + ALL, + CURRENT, + FUTURE, + PAST, + REJECTED, + WAITING; + + public static Optional from(String stringState) { + for (BookingState state : values()) { + if (state.name().equalsIgnoreCase(stringState)) { + return Optional.of(state); + } + } + return Optional.empty(); + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/dto/Status.java b/gateway/src/main/java/ru/practicum/shareit/booking/dto/Status.java new file mode 100644 index 0000000..8ff7dcf --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/dto/Status.java @@ -0,0 +1,8 @@ +package ru.practicum.shareit.booking.dto; + +public enum Status { + WAITING, + APPROVED, + REJECTED, + CANCELED +} diff --git a/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java b/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java new file mode 100644 index 0000000..db39aee --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java @@ -0,0 +1,120 @@ +package ru.practicum.shareit.client; + +import org.springframework.http.*; +import org.springframework.lang.Nullable; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.Map; + +public class BaseClient { + protected final RestTemplate rest; + + public BaseClient(RestTemplate rest) { + this.rest = rest; + } + + protected ResponseEntity get(String path) { + return get(path, null, null); + } + + protected ResponseEntity get(String path, Long userId) { + return get(path, userId, null); + } + + protected ResponseEntity get(String path, Long userId, @Nullable Map parameters) { + return makeAndSendRequest(HttpMethod.GET, path, userId, parameters, null); + } + + protected ResponseEntity post(String path, T body) { + return post(path, null, null, body); + } + + protected ResponseEntity post(String path, Long userId, T body) { + return post(path, userId, null, body); + } + + protected ResponseEntity post(String path, Long userId, @Nullable Map parameters, T body) { + return makeAndSendRequest(HttpMethod.POST, path, userId, parameters, body); + } + + protected ResponseEntity put(String path, Long userId, T body) { + return put(path, userId, null, body); + } + + protected ResponseEntity put(String path, Long userId, @Nullable Map parameters, T body) { + return makeAndSendRequest(HttpMethod.PUT, path, userId, parameters, body); + } + + protected ResponseEntity patch(String path, T body) { + return patch(path, null, null, body); + } + + protected ResponseEntity patch(String path, Long userId) { + return patch(path, userId, null, null); + } + + protected ResponseEntity patch(String path, Long userId, T body) { + return patch(path, userId, null, body); + } + + protected ResponseEntity patch(String path, Long userId, @Nullable Map parameters, T body) { + return makeAndSendRequest(HttpMethod.PATCH, path, userId, parameters, body); + } + + protected ResponseEntity delete(String path) { + return delete(path, null, null); + } + + protected ResponseEntity delete(String path, Long userId) { + return delete(path, userId, null); + } + + protected ResponseEntity delete(String path, Long userId, @Nullable Map parameters) { + return makeAndSendRequest(HttpMethod.DELETE, path, userId, parameters, null); + } + + private ResponseEntity makeAndSendRequest(HttpMethod method, String path, Long userId, @Nullable Map parameters, @Nullable T body) { + HttpEntity requestEntity = new HttpEntity<>(body, defaultHeaders(userId)); + + ResponseEntity shareitServerResponse; + try { + if (parameters != null) { + shareitServerResponse = rest.exchange(path, method, requestEntity, Object.class, parameters); + } else { + shareitServerResponse = rest.exchange(path, method, requestEntity, Object.class); + } + } catch (HttpStatusCodeException e) { + return ResponseEntity.status(e.getStatusCode()).body(e.getResponseBodyAsByteArray()); + } + if (shareitServerResponse == null) { + return null; + } + return prepareGatewayResponse(shareitServerResponse); + } + + private HttpHeaders defaultHeaders(Long userId) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); + if (userId != null) { + headers.set("X-Sharer-User-Id", String.valueOf(userId)); + } + return headers; + } + + private static ResponseEntity prepareGatewayResponse(ResponseEntity response) { + if (response.getStatusCode().is2xxSuccessful()) { + return response; + } + + ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.status(response.getStatusCode()); + + if (response.hasBody()) { + return responseBuilder.body(response.getBody()); + } + + return responseBuilder.build(); + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/comment/dto/CommentDtoRequestCreate.java b/gateway/src/main/java/ru/practicum/shareit/comment/dto/CommentDtoRequestCreate.java new file mode 100644 index 0000000..991c544 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/comment/dto/CommentDtoRequestCreate.java @@ -0,0 +1,8 @@ +package ru.practicum.shareit.comment.dto; + +import jakarta.validation.constraints.NotBlank; + +public record CommentDtoRequestCreate( + @NotBlank String text +) { +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/exception/ApplicationExceptionHandler.java b/gateway/src/main/java/ru/practicum/shareit/exception/ApplicationExceptionHandler.java new file mode 100644 index 0000000..cd08e5a --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/exception/ApplicationExceptionHandler.java @@ -0,0 +1,48 @@ +package ru.practicum.shareit.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class ApplicationExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ProblemDetail handleUserNotFound(MethodArgumentNotValidException ex) { + ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage()); + problem.setTitle("Ошибка валидации."); + problem.setProperty("error", "Ошибка валидации."); + return problem; + } + + @ExceptionHandler(ExceptionItemNotFound.class) + public ProblemDetail handleUserNotFound(ExceptionItemNotFound ex) { + ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()); + problem.setTitle("Запрошенная вещь не найдена."); + return problem; + } + + @ExceptionHandler(ExceptionUserEmailExists.class) + public ProblemDetail handleUserNotFound(ExceptionUserEmailExists ex) { + ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage()); + problem.setTitle("Электронная почта уже занята другим пользователем."); + return problem; + } + + @ExceptionHandler(ExceptionUserNotFound.class) + public ProblemDetail handleUserNotFound(ExceptionUserNotFound ex) { + ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()); + problem.setTitle("Запрошенный пользователь не найден."); + return problem; + } + + @ExceptionHandler(Exception.class) + public ProblemDetail handleUserNotFound(Exception ex) { + ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage()); + problem.setTitle("Внутренняя ошибка сервера."); + return problem; + } + +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionBookingForbiddenOperation.java b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionBookingForbiddenOperation.java new file mode 100644 index 0000000..d9d35eb --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionBookingForbiddenOperation.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionBookingForbiddenOperation extends RuntimeException { + public ExceptionBookingForbiddenOperation(String message) { + super(message); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotAvailable.java b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotAvailable.java new file mode 100644 index 0000000..d3923e4 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotAvailable.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionBookingNotAvailable extends RuntimeException { + public ExceptionBookingNotAvailable(String message) { + super(message); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotFound.java b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotFound.java new file mode 100644 index 0000000..8e91d8b --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotFound.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionBookingNotFound extends RuntimeException { + public ExceptionBookingNotFound(String message) { + super(message); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotOwner.java b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotOwner.java new file mode 100644 index 0000000..aceced3 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotOwner.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionBookingNotOwner extends RuntimeException { + public ExceptionBookingNotOwner(String message) { + super(message); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionInvalidBookingState.java b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionInvalidBookingState.java new file mode 100644 index 0000000..be9a6f4 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionInvalidBookingState.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionInvalidBookingState extends RuntimeException { + public ExceptionInvalidBookingState(String message) { + super(message); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionItemNotFound.java b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionItemNotFound.java new file mode 100644 index 0000000..5667113 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionItemNotFound.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionItemNotFound extends RuntimeException { + public ExceptionItemNotFound(String message) { + super(message); + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionItemRequestNotFound.java b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionItemRequestNotFound.java new file mode 100644 index 0000000..b251df6 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionItemRequestNotFound.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionItemRequestNotFound extends RuntimeException { + public ExceptionItemRequestNotFound(String message) { + super(message); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionNotActiveBooking.java b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionNotActiveBooking.java new file mode 100644 index 0000000..6d695ff --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionNotActiveBooking.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionNotActiveBooking extends RuntimeException { + public ExceptionNotActiveBooking(String message) { + super(message); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionUserEmailExists.java b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionUserEmailExists.java new file mode 100644 index 0000000..93f8745 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionUserEmailExists.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionUserEmailExists extends RuntimeException { + public ExceptionUserEmailExists(String message) { + super(message); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionUserNotFound.java b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionUserNotFound.java new file mode 100644 index 0000000..40d3fbd --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/exception/ExceptionUserNotFound.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionUserNotFound extends RuntimeException { + public ExceptionUserNotFound(String message) { + super(message); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/item/ItemClient.java b/gateway/src/main/java/ru/practicum/shareit/item/ItemClient.java new file mode 100644 index 0000000..09fc4b9 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/ItemClient.java @@ -0,0 +1,56 @@ +package ru.practicum.shareit.item; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.util.DefaultUriBuilderFactory; +import ru.practicum.shareit.client.BaseClient; +import ru.practicum.shareit.comment.dto.CommentDtoRequestCreate; +import ru.practicum.shareit.item.dto.ItemDtoRequestCreate; +import ru.practicum.shareit.item.dto.ItemDtoRequestUpdate; + +@Slf4j +@Service +public class ItemClient extends BaseClient { + @Autowired + public ItemClient(@Value("http://shareit-server:9090") String serverUrl, RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + "/items")) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + } + + public ResponseEntity create(Long userId, ItemDtoRequestCreate item) { + return post("", userId, item); + } + + public ResponseEntity update(Long itemId, Long userId, ItemDtoRequestUpdate item) { + return patch("/" + itemId, userId, item); + } + + public ResponseEntity delete(Long itemId) { + return delete("/" + itemId); + } + + public ResponseEntity getById(Long requesterId, Long itemId) { + return get("/" + itemId, requesterId); + } + + public ResponseEntity getByUserId(Long userId) { + return get("/", userId); + } + + public ResponseEntity search(String text) { + return get("/search?text=" + text); + } + + public ResponseEntity createComment(Long userId, Long itemId, CommentDtoRequestCreate comment) { + return post("/" + itemId + "/comment", userId, comment); + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/item/ItemController.java b/gateway/src/main/java/ru/practicum/shareit/item/ItemController.java new file mode 100644 index 0000000..16716a3 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -0,0 +1,86 @@ +package ru.practicum.shareit.item; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.comment.dto.CommentDtoRequestCreate; +import ru.practicum.shareit.item.dto.ItemDtoRequestCreate; +import ru.practicum.shareit.item.dto.ItemDtoRequestUpdate; + +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/items") +public class ItemController { + private final ItemClient itemClient; + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public ResponseEntity create( + @RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId, + @Validated @RequestBody ItemDtoRequestCreate item + ) { + return itemClient.create(userId, item); + } + + @ResponseStatus(HttpStatus.OK) + @PatchMapping("/{itemId}") + public ResponseEntity update( + @PathVariable @Positive Long itemId, + @Validated @RequestBody ItemDtoRequestUpdate item, + @RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId + ) { + return itemClient.update(itemId, userId, item); + } + + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping("/{itemId}") + public ResponseEntity delete( + @PathVariable @Positive Long itemId + ) { + return itemClient.delete(itemId); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/{itemId}") + public ResponseEntity getById( + @RequestHeader(value = "X-Sharer-User-Id") @Positive Long requesterId, + @PathVariable @Positive Long itemId + ) { + return itemClient.getById(requesterId, itemId); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping + public ResponseEntity getByUserId( + @RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId + ) { + return itemClient.getByUserId(userId); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/search") + public ResponseEntity search( + @RequestParam(name = "text") String text + ) { + if (!StringUtils.hasText(text)) + return null; + return itemClient.search(text); + } + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping("/{itemId}/comment") + public ResponseEntity addComment( + @PathVariable("itemId") @Positive Long itemId, + @RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId, + @Valid @RequestBody CommentDtoRequestCreate comment + ) { + return itemClient.createComment(userId, itemId, comment); + } + +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/item/dto/ItemDtoRequestCreate.java b/gateway/src/main/java/ru/practicum/shareit/item/dto/ItemDtoRequestCreate.java new file mode 100644 index 0000000..fcf8b8f --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/dto/ItemDtoRequestCreate.java @@ -0,0 +1,15 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record ItemDtoRequestCreate( + @NotBlank(message = "Название вещи не может быть пустым") + String name, + @NotBlank(message = "Описание вещи не может быть пустым") + String description, + @NotNull(message = "Необходимо указать статус доступа к аренде") + Boolean available, + Long requestId +) { +} diff --git a/gateway/src/main/java/ru/practicum/shareit/item/dto/ItemDtoRequestUpdate.java b/gateway/src/main/java/ru/practicum/shareit/item/dto/ItemDtoRequestUpdate.java new file mode 100644 index 0000000..3608caf --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/dto/ItemDtoRequestUpdate.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.item.dto; + +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; + +public record ItemDtoRequestUpdate( + String name, + String description, + Boolean available, + ItemRequestDtoRequestCreate request +) { +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/item/dto/ItemDtoResponse.java b/gateway/src/main/java/ru/practicum/shareit/item/dto/ItemDtoResponse.java new file mode 100644 index 0000000..489f49e --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/dto/ItemDtoResponse.java @@ -0,0 +1,14 @@ +package ru.practicum.shareit.item.dto; + +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoResponse; + +public record ItemDtoResponse( + Long id, + String name, + String description, + boolean available, + UserDtoResponse userDtoResponse, + ItemRequestDtoRequestCreate request +) { +} diff --git a/gateway/src/main/java/ru/practicum/shareit/request/ItemRequestClient.java b/gateway/src/main/java/ru/practicum/shareit/request/ItemRequestClient.java new file mode 100644 index 0000000..39c33a2 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/request/ItemRequestClient.java @@ -0,0 +1,47 @@ +package ru.practicum.shareit.request; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.util.DefaultUriBuilderFactory; +import ru.practicum.shareit.client.BaseClient; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; + +import java.util.Map; + +@Service +public class ItemRequestClient extends BaseClient { + @Autowired + public ItemRequestClient(@Value("http://shareit-server:9090") String serverUrl, RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + "/requests")) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + } + + public ResponseEntity create(Long userId, ItemRequestDtoRequestCreate itemRequest) { + return post("", userId, itemRequest); + } + + public ResponseEntity getAllRequestByUserId(Long userId) { + return get("", userId); + } + + public ResponseEntity getRequestById(long requestId, Long userId) { + return get("/" + requestId, userId); + } + + public ResponseEntity getAll(int from, int quantity, Long userId) { + Map parameters = Map.of( + "from", from, + "quantity", quantity); + + return get("/all?from={from}&quantity={quantity}", userId, parameters); + } + +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/request/ItemRequestController.java b/gateway/src/main/java/ru/practicum/shareit/request/ItemRequestController.java new file mode 100644 index 0000000..eff0292 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/request/ItemRequestController.java @@ -0,0 +1,55 @@ +package ru.practicum.shareit.request; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; + +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping(path = "/requests") +public class ItemRequestController { + private final ItemRequestClient itemRequestClient; + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public ResponseEntity create( + @RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId, + @Valid @RequestBody ItemRequestDtoRequestCreate itemRequest + ) { + return itemRequestClient.create(userId, itemRequest); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping + public ResponseEntity getAllRequestByUserId( + @RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId + ) { + return itemRequestClient.getAllRequestByUserId(userId); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/{requestId}") + public ResponseEntity getRequestById( + @PathVariable @Positive Long requestId, + @RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId + ) { + return itemRequestClient.getRequestById(requestId, userId); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/all") + public ResponseEntity getAll( + @RequestParam(defaultValue = "0", required = false) @PositiveOrZero int from, + @RequestParam(defaultValue = "20", required = false) @Positive int quantity, + @RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId + ) { + return itemRequestClient.getAll(from, quantity, userId); + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDtoRequestCreate.java b/gateway/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDtoRequestCreate.java new file mode 100644 index 0000000..93bf404 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDtoRequestCreate.java @@ -0,0 +1,12 @@ +package ru.practicum.shareit.request.dto; + +import jakarta.validation.constraints.NotBlank; + +import java.time.LocalDateTime; + +public record ItemRequestDtoRequestCreate( + @NotBlank(message = "Описание вещи не может быть пустым") + String description, + LocalDateTime created +) { +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/user/UserClient.java b/gateway/src/main/java/ru/practicum/shareit/user/UserClient.java new file mode 100644 index 0000000..8f3fb24 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/user/UserClient.java @@ -0,0 +1,42 @@ +package ru.practicum.shareit.user; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.util.DefaultUriBuilderFactory; +import ru.practicum.shareit.client.BaseClient; +import ru.practicum.shareit.user.dto.UserDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoRequestUpdate; + +@Service +public class UserClient extends BaseClient { + @Autowired + public UserClient(@Value("http://shareit-server:9090") String serverUrl, RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + "/users")) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + } + + public ResponseEntity create(UserDtoRequestCreate user) { + return post("", user); + } + + public ResponseEntity update(Long id, UserDtoRequestUpdate user) { + return patch("/" + id, user); + } + + public ResponseEntity delete(Long id) { + return delete("/" + id); + } + + public ResponseEntity get(Long id) { + return get("/" + id); + } + +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/user/UserController.java b/gateway/src/main/java/ru/practicum/shareit/user/UserController.java new file mode 100644 index 0000000..f3e53eb --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/user/UserController.java @@ -0,0 +1,51 @@ +package ru.practicum.shareit.user; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.user.dto.UserDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoRequestUpdate; + +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/users") +public class UserController { + private final UserClient userClient; + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public ResponseEntity create( + @Valid @RequestBody UserDtoRequestCreate user + ) { + return userClient.create(user); + } + + @ResponseStatus(HttpStatus.OK) + @PatchMapping("/{id}") + public ResponseEntity update( + @PathVariable @Positive Long id, + @RequestBody UserDtoRequestUpdate user + ) { + return userClient.update(id, user); + } + + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping("/{id}") + public ResponseEntity delete( + @PathVariable @Positive Long id + ) { + return userClient.delete(id); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/{id}") + public ResponseEntity get( + @PathVariable @Positive Long id + ) { + return userClient.get(id); + } + +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/user/dto/UserDtoRequestCreate.java b/gateway/src/main/java/ru/practicum/shareit/user/dto/UserDtoRequestCreate.java new file mode 100644 index 0000000..353a2bb --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/user/dto/UserDtoRequestCreate.java @@ -0,0 +1,13 @@ +package ru.practicum.shareit.user.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record UserDtoRequestCreate( + @NotBlank(message = "Имя пользователя не может быть пустым") + String name, + @NotBlank(message = "Электронная почта пользователя не может быть пустая") + @Email + String email +) { +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/user/dto/UserDtoRequestUpdate.java b/gateway/src/main/java/ru/practicum/shareit/user/dto/UserDtoRequestUpdate.java new file mode 100644 index 0000000..43b178a --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/user/dto/UserDtoRequestUpdate.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.user.dto; + +public record UserDtoRequestUpdate(String name, String email) { +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/user/dto/UserDtoResponse.java b/gateway/src/main/java/ru/practicum/shareit/user/dto/UserDtoResponse.java new file mode 100644 index 0000000..850fb0c --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/user/dto/UserDtoResponse.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.user.dto; + +public record UserDtoResponse(Long id, String name, String email) { +} \ No newline at end of file diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties new file mode 100644 index 0000000..5bb90ae --- /dev/null +++ b/gateway/src/main/resources/application.properties @@ -0,0 +1,3 @@ +logging.level.org.springframework.web.client.RestTemplate=DEBUG +server.port=8080 +shareit-server.url=http://shareit-server:9090 \ No newline at end of file diff --git a/gateway/src/test/java/ru/practicum/shareit/booking/TestBookingClient.java b/gateway/src/test/java/ru/practicum/shareit/booking/TestBookingClient.java new file mode 100644 index 0000000..a85e16c --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/booking/TestBookingClient.java @@ -0,0 +1,132 @@ +package ru.practicum.shareit.booking; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.*; +import org.springframework.web.client.RestTemplate; +import ru.practicum.shareit.booking.dto.BookingDtoRequestCreate; +import ru.practicum.shareit.booking.dto.BookingDtoResponse; +import ru.practicum.shareit.booking.dto.BookingState; +import ru.practicum.shareit.booking.dto.Status; +import ru.practicum.shareit.item.dto.ItemDtoResponse; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoResponse; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class TestBookingClient { + @Mock + private RestTemplateBuilder builder; + @Mock + private RestTemplate restTemplate; + private BookingClient bookingClient; + private final BookingDtoRequestCreate bookingDtoRequestCreate = new BookingDtoRequestCreate( + 1L, + LocalDateTime.now().plusHours(1), + LocalDateTime.now().plusDays(1) + ); + private final UserDtoResponse userDtoResponse = new UserDtoResponse( + 1L, + "Пользователь №1", + "mail@mail.ru" + ); + private final ItemRequestDtoRequestCreate itemRequestDtoRequestCreate = new ItemRequestDtoRequestCreate( + "Описание запроса на вещь №1", + LocalDateTime.now() + ); + private final ItemDtoResponse itemDtoResponse = new ItemDtoResponse( + 1L, + "Название вещи", + "Описание вещи", + true, + userDtoResponse, + itemRequestDtoRequestCreate + ); + private final BookingDtoResponse bookingDtoResponse = new BookingDtoResponse( + 1L, + LocalDateTime.now().plusHours(1), + LocalDateTime.now().plusDays(1), + Status.WAITING, + itemDtoResponse, + userDtoResponse + ); + + @BeforeEach + void setUp() { + when(builder.uriTemplateHandler(any())).thenReturn(builder); + when(builder.requestFactory(any(Supplier.class))).thenReturn(builder); + when(builder.build()).thenReturn(restTemplate); + bookingClient = new BookingClient("", builder); + } + + @Test + void createBooking() { + Mockito + .when(restTemplate.exchange("", HttpMethod.POST, new HttpEntity<>(bookingDtoRequestCreate, + getHeader(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(bookingDtoResponse)); + ResponseEntity response = bookingClient.create(1L, bookingDtoRequestCreate); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(bookingDtoResponse)); + } + + @Test + void approveBooking() { + Mockito + .when(restTemplate.exchange("/1?approved=true", HttpMethod.PATCH, new HttpEntity<>(null, + getHeader(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(bookingDtoResponse)); + ResponseEntity response = bookingClient.approve(1L, 1L, true); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(bookingDtoResponse)); + } + + @Test + void getBooking() { + Mockito + .when(restTemplate.exchange("/1", HttpMethod.GET, new HttpEntity<>(null, + getHeader(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(bookingDtoResponse)); + ResponseEntity response = bookingClient.get(1L, 1L); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(bookingDtoResponse)); + } + + @Test + void getAllBookingCurrentUser() { + Map parameters = Map.of( + "state", BookingState.WAITING.name() + ); + List bookings = List.of(bookingDtoResponse); + Mockito + .when(restTemplate.exchange("?state={state}", HttpMethod.GET, new HttpEntity<>(null, + getHeader(1L)), Object.class, parameters)) + .thenReturn(ResponseEntity.ok(bookings)); + ResponseEntity response = bookingClient.getAllBookingCurrentUser(1L, BookingState.WAITING); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(bookings)); + } + + private HttpHeaders getHeader(Long userId) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); + headers.set("X-Sharer-User-Id", String.valueOf(userId)); + return headers; + } + +} diff --git a/gateway/src/test/java/ru/practicum/shareit/booking/TestBookingController.java b/gateway/src/test/java/ru/practicum/shareit/booking/TestBookingController.java new file mode 100644 index 0000000..bbba207 --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/booking/TestBookingController.java @@ -0,0 +1,139 @@ +package ru.practicum.shareit.booking; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.booking.dto.BookingDtoRequestCreate; +import ru.practicum.shareit.booking.dto.BookingDtoResponse; +import ru.practicum.shareit.booking.dto.BookingState; +import ru.practicum.shareit.booking.dto.Status; +import ru.practicum.shareit.item.dto.ItemDtoResponse; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoResponse; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(controllers = BookingController.class) +class TestBookingController { + @Autowired + ObjectMapper mapper; + @MockBean + BookingClient bookingClient; + @Autowired + private MockMvc mvc; + private final BookingDtoRequestCreate bookingDtoRequestCreate = new BookingDtoRequestCreate( + 1L, + LocalDateTime.now().plusHours(1), + LocalDateTime.now().plusDays(1) + ); + private final UserDtoResponse userDtoResponse = new UserDtoResponse( + 1L, + "Пользователь №1", + "mail@mail.ru" + ); + private final ItemRequestDtoRequestCreate itemRequestDtoRequestCreate = new ItemRequestDtoRequestCreate( + "Описание запроса на вещь №1", + LocalDateTime.now() + ); + private final ItemDtoResponse itemDtoResponse = new ItemDtoResponse( + 1L, + "Название вещи", + "Описание вещи", + true, + userDtoResponse, + itemRequestDtoRequestCreate + ); + private final BookingDtoResponse bookingDtoResponse = new BookingDtoResponse( + 1L, + LocalDateTime.now().plusHours(1), + LocalDateTime.now().plusDays(1), + Status.WAITING, + itemDtoResponse, + userDtoResponse + ); + + @Test + void createBooking() throws Exception { + when(bookingClient.create(1L, bookingDtoRequestCreate)) + .thenReturn(ResponseEntity.ok(bookingDtoResponse)); + mvc.perform(post("/bookings") + .content(mapper.writeValueAsString(bookingDtoRequestCreate)) + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + void approveBooking() throws Exception { + when(bookingClient.approve(1L, 1L, true)) + .thenReturn(ResponseEntity.ok(bookingDtoResponse)); + mvc.perform(patch("/bookings/1?approved=true") + .content(mapper.writeValueAsString(bookingDtoRequestCreate)) + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(bookingDtoResponse.id()), Long.class)) + .andExpect(jsonPath("$.status", is(bookingDtoResponse.status().toString()))); + } + + @Test + void getBooking() throws Exception { + when(bookingClient.get(1L, 1L)) + .thenReturn(ResponseEntity.ok(bookingDtoResponse)); + mvc.perform(get("/bookings/1") + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(bookingDtoResponse.id()), Long.class)) + .andExpect(jsonPath("$.status", is(bookingDtoResponse.status().toString()))); + } + + @Test + void getAllBookingCurrentUser() throws Exception { + when(bookingClient.getAllBookingCurrentUser(1L, BookingState.ALL)) + .thenReturn(ResponseEntity.ok(List.of(bookingDtoResponse))); + mvc.perform(get("/bookings") + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id", is(bookingDtoResponse.id()), Long.class)) + .andExpect(jsonPath("$[0].status", is(bookingDtoResponse.status().toString()))); + } + + @Test + void getAllBookingAllItemCurrentUser() throws Exception { + when(bookingClient.getAllBookingAllItemCurrentUser(1L, BookingState.ALL)) + .thenReturn(ResponseEntity.ok(List.of(bookingDtoResponse))); + mvc.perform(get("/bookings/owner") + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id", is(bookingDtoResponse.id()), Long.class)) + .andExpect(jsonPath("$[0].status", is(bookingDtoResponse.status().toString()))); + } + +} \ No newline at end of file diff --git a/gateway/src/test/java/ru/practicum/shareit/client/TestClient.java b/gateway/src/test/java/ru/practicum/shareit/client/TestClient.java new file mode 100644 index 0000000..a9b2194 --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/client/TestClient.java @@ -0,0 +1,213 @@ +package ru.practicum.shareit.client; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.*; +import org.springframework.web.client.RestTemplate; +import ru.practicum.shareit.user.dto.UserDtoResponse; + +import java.util.List; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +@ExtendWith(MockitoExtension.class) +class TestClient { + @Mock + private RestTemplate restTemplate; + private BaseClient baseClient; + private final UserDtoResponse userDtoResponse = new UserDtoResponse( + 1L, + "Пользователь №1", + "mail@mail.ru" + ); + + @BeforeEach + void setUp() { + baseClient = new BaseClient(restTemplate); + } + + @Test + void getPath() { + Mockito + .when(restTemplate.exchange("/", HttpMethod.GET, new HttpEntity<>(null, + getHeader(null)), Object.class)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = baseClient.get("/"); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + @Test + void getPathAndUserId() { + Mockito + .when(restTemplate.exchange("/", HttpMethod.GET, new HttpEntity<>(null, + getHeader(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = baseClient.get("/", 1L); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + @Test + void getPathAndUserIdAndParameters() { + Map params = Map.of("State", "All"); + Mockito + .when(restTemplate.exchange("/", HttpMethod.GET, new HttpEntity<>(null, + getHeader(1L)), Object.class, params)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = baseClient.get("/", 1L, params); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + @Test + void postPathAndBody() { + Mockito + .when(restTemplate.exchange("/", HttpMethod.POST, new HttpEntity<>(userDtoResponse, + getHeader(null)), Object.class)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = baseClient.post("/", userDtoResponse); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + @Test + void postPathAndUserIdAndBody() { + Mockito + .when(restTemplate.exchange("/", HttpMethod.POST, new HttpEntity<>(userDtoResponse, + getHeader(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = baseClient.post("/", 1L, userDtoResponse); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + @Test + void postPathAndUserIdAndParametersAndBody() { + Map params = Map.of("State", "All"); + Mockito + .when(restTemplate.exchange("/", HttpMethod.POST, new HttpEntity<>(userDtoResponse, + getHeader(1L)), Object.class, params)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = baseClient.post("/", 1L, params, userDtoResponse); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + @Test + void putPathAndUserIdAndBody() { + Mockito + .when(restTemplate.exchange("/", HttpMethod.PUT, new HttpEntity<>(userDtoResponse, + getHeader(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = baseClient.put("/", 1L, userDtoResponse); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + @Test + void putPathAndUserIdAndParametersAndBody() { + Map params = Map.of("State", "All"); + Mockito + .when(restTemplate.exchange("/", HttpMethod.PUT, new HttpEntity<>(userDtoResponse, + getHeader(1L)), Object.class, params)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = baseClient.put("/", 1L, params, userDtoResponse); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + @Test + void patchPathAndBody() { + Mockito + .when(restTemplate.exchange("/", HttpMethod.PATCH, new HttpEntity<>(userDtoResponse, + getHeader(null)), Object.class)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = baseClient.patch("/", userDtoResponse); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + @Test + void patchPathAndUserId() { + Mockito + .when(restTemplate.exchange("/", HttpMethod.PATCH, new HttpEntity<>(null, + getHeader(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = baseClient.patch("/", 1L); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + @Test + void patchPathAndUserIdAndBody() { + Mockito + .when(restTemplate.exchange("/", HttpMethod.PATCH, new HttpEntity<>(userDtoResponse, + getHeader(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = baseClient.patch("/", 1L, userDtoResponse); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + @Test + void patchPathAndUserIdAndParametersAndBody() { + Map params = Map.of("State", "All"); + Mockito + .when(restTemplate.exchange("/", HttpMethod.PATCH, new HttpEntity<>(userDtoResponse, + getHeader(1L)), Object.class, params)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = baseClient.patch("/", 1L, params, userDtoResponse); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + @Test + void deletePath() { + Mockito + .when(restTemplate.exchange("/", HttpMethod.DELETE, new HttpEntity<>(null, + getHeader(null)), Object.class)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = baseClient.delete("/"); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + @Test + void deletePathAndUserId() { + Mockito + .when(restTemplate.exchange("/", HttpMethod.DELETE, new HttpEntity<>(null, + getHeader(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = baseClient.delete("/", 1L); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + @Test + void deletePathAndUserIdAndParameters() { + Map params = Map.of("State", "All"); + Mockito + .when(restTemplate.exchange("/", HttpMethod.DELETE, new HttpEntity<>(null, + getHeader(1L)), Object.class, params)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = baseClient.delete("/", 1L, params); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + private HttpHeaders getHeader(Long userId) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); + if (userId != null) + headers.set("X-Sharer-User-Id", String.valueOf(userId)); + return headers; + } +} \ No newline at end of file diff --git a/gateway/src/test/java/ru/practicum/shareit/item/TestItemClient.java b/gateway/src/test/java/ru/practicum/shareit/item/TestItemClient.java new file mode 100644 index 0000000..8374e43 --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/item/TestItemClient.java @@ -0,0 +1,165 @@ +package ru.practicum.shareit.item; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.*; +import org.springframework.web.client.RestTemplate; +import ru.practicum.shareit.comment.dto.CommentDtoRequestCreate; +import ru.practicum.shareit.item.dto.ItemDtoRequestCreate; +import ru.practicum.shareit.item.dto.ItemDtoRequestUpdate; +import ru.practicum.shareit.item.dto.ItemDtoResponse; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoResponse; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.util.AssertionErrors.assertNull; + +@ExtendWith(MockitoExtension.class) +class TestItemClient { + @Mock + private RestTemplateBuilder builder; + @Mock + private RestTemplate restTemplate; + private ItemClient itemClient; + private final UserDtoResponse userDtoResponse = new UserDtoResponse( + 1L, + "Пользователь №1", + "mail@mail.ru" + ); + private final ItemRequestDtoRequestCreate itemRequestDtoRequestCreate = new ItemRequestDtoRequestCreate( + "Описание запроса на вещь №1", + LocalDateTime.now() + ); + private final ItemDtoResponse itemDtoResponse = new ItemDtoResponse( + 1L, + "Название вещи", + "Описание вещи", + true, + userDtoResponse, + itemRequestDtoRequestCreate + ); + private final ItemDtoRequestCreate itemDtoRequestCreate = new ItemDtoRequestCreate( + "Название вещи", + "Описание вещи", + true, + 2L + ); + private final ItemDtoRequestUpdate itemDtoRequestUpdate = new ItemDtoRequestUpdate( + "Название вещи", + "Описание вещи", + false, + itemRequestDtoRequestCreate + ); + private final CommentDtoRequestCreate commentDtoRequestCreate = new CommentDtoRequestCreate( + "Текст комментария" + ); + + @BeforeEach + void setUp() { + when(builder.uriTemplateHandler(any())).thenReturn(builder); + when(builder.requestFactory(any(Supplier.class))).thenReturn(builder); + when(builder.build()).thenReturn(restTemplate); + itemClient = new ItemClient("", builder); + } + + @Test + void createItem() { + Mockito + .when(restTemplate.exchange("", HttpMethod.POST, new HttpEntity<>(itemDtoRequestCreate, + getHeader(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(itemDtoResponse)); + ResponseEntity response = itemClient.create(1L, itemDtoRequestCreate); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(itemDtoResponse)); + } + + @Test + void updateItem() { + Mockito + .when(restTemplate.exchange("/1", HttpMethod.PATCH, new HttpEntity<>(itemDtoRequestUpdate, + getHeader(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(itemDtoResponse)); + ResponseEntity response = itemClient.update(1L, 1L, itemDtoRequestUpdate); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(itemDtoResponse)); + } + + @Test + void deleteItem() { + Mockito + .lenient().when(restTemplate.exchange("/1", HttpMethod.PATCH, new HttpEntity<>(null, + getHeader(null)), Object.class)) + .thenReturn(ResponseEntity.ok(null)); + ResponseEntity response = itemClient.delete(1L); + assertNull("Ответ должен быть null.", response); + } + + @Test + void getItemById() { + Mockito + .when(restTemplate.exchange("/1", HttpMethod.GET, new HttpEntity<>(null, + getHeader(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(itemDtoResponse)); + ResponseEntity response = itemClient.getById(1L, 1L); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(itemDtoResponse)); + } + + @Test + void getItemByUserId() { + List items = List.of(itemDtoResponse); + Mockito + .when(restTemplate.exchange("/", HttpMethod.GET, new HttpEntity<>(null, + getHeader(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(items)); + ResponseEntity response = itemClient.getByUserId(1L); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(items)); + } + + @Test + void searchItem() { + Map parameters = Map.of( + "text", "Новый поиск" + ); + Mockito + .lenient().when(restTemplate.exchange("/search?text={text}", HttpMethod.GET, new HttpEntity<>(null, + getHeader(null)), Object.class, parameters)) + .thenReturn(ResponseEntity.ok(null)); + ResponseEntity response = itemClient.search("Новый поиск"); + assertNull("Ответ должен быть null.", response); + } + + @Test + void createComment() { + Mockito + .when(restTemplate.exchange("/1/comment", HttpMethod.POST, new HttpEntity<>(commentDtoRequestCreate, + getHeader(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(null)); + ResponseEntity response = itemClient.createComment(1L, 1L, commentDtoRequestCreate); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(null)); + } + + private HttpHeaders getHeader(Long userId) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); + if (userId != null) + headers.set("X-Sharer-User-Id", String.valueOf(userId)); + return headers; + } +} \ No newline at end of file diff --git a/gateway/src/test/java/ru/practicum/shareit/item/TestItemController.java b/gateway/src/test/java/ru/practicum/shareit/item/TestItemController.java new file mode 100644 index 0000000..521db2f --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/item/TestItemController.java @@ -0,0 +1,155 @@ +package ru.practicum.shareit.item; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.comment.dto.CommentDtoRequestCreate; +import ru.practicum.shareit.item.dto.ItemDtoRequestCreate; +import ru.practicum.shareit.item.dto.ItemDtoRequestUpdate; +import ru.practicum.shareit.item.dto.ItemDtoResponse; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoResponse; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(controllers = ItemController.class) +class TestItemController { + @Autowired + ObjectMapper mapper; + @MockBean + ItemClient itemClient; + @Autowired + private MockMvc mvc; + private final UserDtoResponse userDtoResponse = new UserDtoResponse( + 1L, + "Пользователь №1", + "mail@mail.ru" + ); + private final ItemRequestDtoRequestCreate itemRequestDtoRequestCreate = new ItemRequestDtoRequestCreate( + "Описание запроса на вещь №1", + LocalDateTime.now() + ); + private final ItemDtoResponse itemDtoResponse = new ItemDtoResponse( + 1L, + "Название вещи", + "Описание вещи", + true, + userDtoResponse, + itemRequestDtoRequestCreate + ); + private final ItemDtoRequestCreate itemDtoRequestCreate = new ItemDtoRequestCreate( + "Название вещи", + "Описание вещи", + true, + 2L + ); + private final ItemDtoRequestUpdate itemDtoRequestUpdate = new ItemDtoRequestUpdate( + "Название вещи", + "Описание вещи", + false, + itemRequestDtoRequestCreate + ); + private final CommentDtoRequestCreate commentDtoRequestCreate = new CommentDtoRequestCreate( + "Текст комментария" + ); + + @Test + void createItem() throws Exception { + when(itemClient.create(1L, itemDtoRequestCreate)) + .thenReturn(ResponseEntity.ok(itemDtoResponse)); + mvc.perform(post("/items") + .content(mapper.writeValueAsString(itemDtoRequestCreate)) + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + void updateItem() throws Exception { + when(itemClient.update(1L, 1L, itemDtoRequestUpdate)) + .thenReturn(ResponseEntity.ok(itemDtoRequestUpdate)); + mvc.perform(patch("/items/1") + .content(mapper.writeValueAsString(itemDtoRequestUpdate)) + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name", is(itemDtoRequestUpdate.name()))) + .andExpect(jsonPath("$.description", is(itemDtoRequestUpdate.description()))); + } + + @Test + void deleteItem() throws Exception { + mvc.perform(delete("/items/1").characterEncoding(StandardCharsets.UTF_8)) + .andExpect(status().is2xxSuccessful()); + } + + @Test + void getItemById() throws Exception { + when(itemClient.getById(1L, 1L)) + .thenReturn(ResponseEntity.ok(itemDtoResponse)); + mvc.perform(get("/items/1") + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name", is(itemDtoRequestUpdate.name()))) + .andExpect(jsonPath("$.description", is(itemDtoRequestUpdate.description()))); + } + + @Test + void getItemByUserId() throws Exception { + when(itemClient.getByUserId(1L)) + .thenReturn(ResponseEntity.ok(List.of(itemDtoResponse))); + mvc.perform(get("/items") + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name", is(itemDtoRequestUpdate.name()))) + .andExpect(jsonPath("$[0].description", is(itemDtoRequestUpdate.description()))); + + } + + @Test + void searchItem() throws Exception { + when(itemClient.search("Название вещи")) + .thenReturn(ResponseEntity.ok(List.of(itemDtoResponse))); + mvc.perform(get("/items/search?text=Название вещи").characterEncoding(StandardCharsets.UTF_8)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name", is(itemDtoRequestUpdate.name()))) + .andExpect(jsonPath("$[0].description", is(itemDtoRequestUpdate.description()))); + } + + @Test + void createComment() throws Exception { + when(itemClient.createComment(1L, 1L, commentDtoRequestCreate)) + .thenReturn(ResponseEntity.ok(commentDtoRequestCreate)); + mvc.perform(post("/items/1/comment") + .content(mapper.writeValueAsString(commentDtoRequestCreate)) + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + +} \ No newline at end of file diff --git a/gateway/src/test/java/ru/practicum/shareit/request/TestItemRequestClient.java b/gateway/src/test/java/ru/practicum/shareit/request/TestItemRequestClient.java new file mode 100644 index 0000000..d16a707 --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/request/TestItemRequestClient.java @@ -0,0 +1,95 @@ +package ru.practicum.shareit.request; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.*; +import org.springframework.web.client.RestTemplate; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.function.Supplier; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.util.AssertionErrors.assertNull; + +@ExtendWith(MockitoExtension.class) +class TestItemRequestClient { + @Mock + private RestTemplateBuilder builder; + @Mock + private RestTemplate restTemplate; + private ItemRequestClient itemRequestClient; + private final ItemRequestDtoRequestCreate itemRequestDtoRequestCreate = new ItemRequestDtoRequestCreate( + "Описание запроса на вещь №1", + LocalDateTime.now() + ); + + @BeforeEach + void setUp() { + when(builder.uriTemplateHandler(any())).thenReturn(builder); + when(builder.requestFactory(any(Supplier.class))).thenReturn(builder); + when(builder.build()).thenReturn(restTemplate); + itemRequestClient = new ItemRequestClient("", builder); + } + + @Test + void createItemRequest() { + Mockito + .when(restTemplate.exchange("", HttpMethod.POST, new HttpEntity<>(itemRequestDtoRequestCreate, + getHeaders(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(itemRequestDtoRequestCreate)); + ResponseEntity response = itemRequestClient.create(1L, itemRequestDtoRequestCreate); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(itemRequestDtoRequestCreate)); + } + + @Test + void getAllItemRequestByUserId() { + List requests = List.of(itemRequestDtoRequestCreate); + Mockito + .when(restTemplate.exchange("", HttpMethod.GET, new HttpEntity<>(null, + getHeaders(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(requests)); + ResponseEntity response = itemRequestClient.getAllRequestByUserId(1L); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(requests)); + } + + @Test + void getItemRequestById() { + Mockito + .when(restTemplate.exchange("/1", HttpMethod.GET, new HttpEntity<>(null, + getHeaders(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(itemRequestDtoRequestCreate)); + ResponseEntity response = itemRequestClient.getRequestById(1L, 1L); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(itemRequestDtoRequestCreate)); + } + + @Test + void getAllItemRequest() { + Mockito + .lenient().when(restTemplate.exchange("/all?from=0&quantity=20", HttpMethod.GET, new HttpEntity<>(null, + getHeaders(1L)), Object.class)) + .thenReturn(ResponseEntity.ok(null)); + ResponseEntity response = itemRequestClient.getAll(0, 20, 2L); + assertNull("Ответ должен быть null.", response); + } + + private HttpHeaders getHeaders(Long userId) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); + headers.set("X-Sharer-User-Id", String.valueOf(userId)); + return headers; + } +} \ No newline at end of file diff --git a/gateway/src/test/java/ru/practicum/shareit/request/TestItemRequestController.java b/gateway/src/test/java/ru/practicum/shareit/request/TestItemRequestController.java new file mode 100644 index 0000000..3abd03c --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/request/TestItemRequestController.java @@ -0,0 +1,87 @@ +package ru.practicum.shareit.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(controllers = ItemRequestController.class) +class TestItemRequestController { + @Autowired + ObjectMapper mapper; + @MockBean + ItemRequestClient itemRequestClient; + @Autowired + private MockMvc mvc; + private final ItemRequestDtoRequestCreate itemRequestDtoRequestCreate = new ItemRequestDtoRequestCreate( + "Описание запроса на вещь №1", + LocalDateTime.now() + ); + + @Test + void createItemRequest() throws Exception { + when(itemRequestClient.create(1L, itemRequestDtoRequestCreate)) + .thenReturn(ResponseEntity.ok(itemRequestDtoRequestCreate)); + mvc.perform(post("/requests") + .content(mapper.writeValueAsString(itemRequestDtoRequestCreate)) + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.description", is(itemRequestDtoRequestCreate.description()))); + } + + @Test + void getAllItemRequestByUserId() throws Exception { + when(itemRequestClient.getAllRequestByUserId(1L)) + .thenReturn(ResponseEntity.ok(List.of(itemRequestDtoRequestCreate))); + mvc.perform(get("/requests") + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].description", is(itemRequestDtoRequestCreate.description()))); + } + + @Test + void getAllItemRequestById() throws Exception { + when(itemRequestClient.getRequestById(1L, 1L)) + .thenReturn(ResponseEntity.ok(List.of(itemRequestDtoRequestCreate))); + mvc.perform(get("/requests/1") + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + void getAllItemRequest() throws Exception { + when(itemRequestClient.getAll(0, 20, 1L)) + .thenReturn(ResponseEntity.ok(List.of(itemRequestDtoRequestCreate))); + mvc.perform(get("/requests/all") + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } +} \ No newline at end of file diff --git a/gateway/src/test/java/ru/practicum/shareit/user/TestUserClient.java b/gateway/src/test/java/ru/practicum/shareit/user/TestUserClient.java new file mode 100644 index 0000000..4935dca --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/user/TestUserClient.java @@ -0,0 +1,104 @@ +package ru.practicum.shareit.user; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.*; +import org.springframework.web.client.RestTemplate; +import ru.practicum.shareit.user.dto.UserDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoRequestUpdate; +import ru.practicum.shareit.user.dto.UserDtoResponse; + +import java.util.List; +import java.util.function.Supplier; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TestUserClient { + @Mock + private RestTemplateBuilder builder; + @Mock + private RestTemplate restTemplate; + private UserClient userClient; + private final UserDtoResponse userDtoResponse = new UserDtoResponse( + 1L, + "Пользователь №1", + "mail@mail.ru" + ); + private final UserDtoRequestCreate userDtoRequestCreate = new UserDtoRequestCreate( + "Пользователь №1", + "mail@mail.ru" + ); + private final UserDtoRequestUpdate userDtoRequestUpdate = new UserDtoRequestUpdate( + "Пользователь №1 обновлен", + "mail@mail.ru" + ); + + @BeforeEach + void setUp() { + when(builder.uriTemplateHandler(any())).thenReturn(builder); + when(builder.requestFactory(any(Supplier.class))).thenReturn(builder); + when(builder.build()).thenReturn(restTemplate); + userClient = new UserClient("", builder); + } + + @Test + void createUser() { + Mockito + .when(restTemplate.exchange("", HttpMethod.POST, new HttpEntity<>(userDtoRequestCreate, + getHeaders(null)), Object.class)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = userClient.create(userDtoRequestCreate); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + @Test + void updateUser() { + Mockito + .when(restTemplate.exchange("/1", HttpMethod.PATCH, new HttpEntity<>(userDtoRequestUpdate, + getHeaders(null)), Object.class)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = userClient.update(1L, userDtoRequestUpdate); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + @Test + void deleteUser() { + Mockito + .when(restTemplate.exchange("/1", HttpMethod.DELETE, new HttpEntity<>(null, + getHeaders(null)), Object.class)) + .thenReturn(ResponseEntity.ok().build()); + ResponseEntity response = userClient.delete(1L); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + } + + @Test + void getUser() { + Mockito + .when(restTemplate.exchange("/1", HttpMethod.GET, new HttpEntity<>(null, + getHeaders(null)), Object.class)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + ResponseEntity response = userClient.get(1L); + assertThat(response.getStatusCode(), equalTo(HttpStatus.OK)); + assertThat(response.getBody(), equalTo(userDtoResponse)); + } + + private HttpHeaders getHeaders(Long userId) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); + if (userId != null) + headers.set("X-Sharer-User-Id", String.valueOf(userId)); + return headers; + } +} \ No newline at end of file diff --git a/gateway/src/test/java/ru/practicum/shareit/user/TestUserController.java b/gateway/src/test/java/ru/practicum/shareit/user/TestUserController.java new file mode 100644 index 0000000..abb4533 --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/user/TestUserController.java @@ -0,0 +1,92 @@ +package ru.practicum.shareit.user; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.user.dto.UserDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoRequestUpdate; +import ru.practicum.shareit.user.dto.UserDtoResponse; + +import java.nio.charset.StandardCharsets; + +import static org.mockito.Mockito.when; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(controllers = UserController.class) +class TestUserController { + @Autowired + private ObjectMapper mapper; + @MockBean + private UserClient userClient; + @Autowired + private MockMvc mvc; + private final UserDtoResponse userDtoResponse = new UserDtoResponse( + 1L, + "Пользователь №1", + "mail@mail.ru" + ); + private final UserDtoRequestCreate userDtoRequestCreate = new UserDtoRequestCreate( + "Пользователь №1", + "mail@mail.ru" + ); + private final UserDtoRequestUpdate userDtoRequestUpdate = new UserDtoRequestUpdate( + "Пользователь №1 обновлен", + "mail@mail.ru" + ); + + @Test + void createUser() throws Exception { + when(userClient.create(userDtoRequestCreate)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + mvc.perform(post("/users") + .content(mapper.writeValueAsString(userDtoRequestCreate)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + void updateUser() throws Exception { + when(userClient.update(1L, userDtoRequestUpdate)) + .thenReturn(ResponseEntity.ok(userDtoRequestUpdate)); + mvc.perform(patch("/users/1") + .content(mapper.writeValueAsString(userDtoRequestUpdate)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name", is(userDtoRequestUpdate.name()))) + .andExpect(jsonPath("$.email", is(userDtoRequestUpdate.email()))); + } + + @Test + void deleteUser() throws Exception { + mvc.perform(delete("/users/1") + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()); + } + + @Test + void getUser() throws Exception { + when(userClient.get(1L)) + .thenReturn(ResponseEntity.ok(userDtoResponse)); + mvc.perform(get("/users/1") + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name", is(userDtoResponse.name()))) + .andExpect(jsonPath("$.email", is(userDtoResponse.email()))); + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2db888c..79cba37 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,7 @@ ru.practicum shareit + pom 0.0.1-SNAPSHOT ShareIt @@ -19,72 +20,29 @@ 21 - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - org.postgresql - postgresql - runtime - - - - org.projectlombok - lombok - true - - - - com.h2database - h2 - test - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-starter-validation - - + + gateway + server + - - - src/main/resources - true - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - + + org.springframework.boot + spring-boot-maven-plugin + + + true + + + + org.projectlombok + lombok + + + + org.apache.maven.plugins maven-surefire-plugin @@ -231,17 +189,5 @@ - - coverage - - - - org.jacoco - jacoco-maven-plugin - - - - - - + \ No newline at end of file diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..0ff1817 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:21-jre-jammy +VOLUME /tmp +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file diff --git a/server/pom.xml b/server/pom.xml new file mode 100644 index 0000000..5115a5d --- /dev/null +++ b/server/pom.xml @@ -0,0 +1,113 @@ + + + 4.0.0 + + ru.practicum + shareit + 0.0.1-SNAPSHOT + + + shareit-server + 0.0.1-SNAPSHOT + + ShareIt Server + + + 1.6.3 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.postgresql + postgresql + runtime + + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + + + + + diff --git a/src/main/java/ru/practicum/shareit/ShareItApp.java b/server/src/main/java/ru/practicum/shareit/ShareItServer.java similarity index 73% rename from src/main/java/ru/practicum/shareit/ShareItApp.java rename to server/src/main/java/ru/practicum/shareit/ShareItServer.java index a00ad56..303541d 100644 --- a/src/main/java/ru/practicum/shareit/ShareItApp.java +++ b/server/src/main/java/ru/practicum/shareit/ShareItServer.java @@ -4,10 +4,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class ShareItApp { +public class ShareItServer { public static void main(String[] args) { - SpringApplication.run(ShareItApp.class, args); + SpringApplication.run(ShareItServer.class, args); } } diff --git a/server/src/main/java/ru/practicum/shareit/booking/contoller/BookingController.java b/server/src/main/java/ru/practicum/shareit/booking/contoller/BookingController.java new file mode 100644 index 0000000..9390607 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/contoller/BookingController.java @@ -0,0 +1,63 @@ +package ru.practicum.shareit.booking.contoller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.booking.dto.BookingDtoRequestCreate; +import ru.practicum.shareit.booking.dto.BookingDtoResponse; +import ru.practicum.shareit.booking.model.BookingState; +import ru.practicum.shareit.booking.service.BookingService; + +import java.util.List; + +@RestController +@RequestMapping("/bookings") +@RequiredArgsConstructor +@Validated +public class BookingController { + private final BookingService bookingService; + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public BookingDtoResponse create( + @RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId, + @Valid @RequestBody BookingDtoRequestCreate booking + ) { + return bookingService.create(userId, booking); + } + + @ResponseStatus(HttpStatus.OK) + @PatchMapping("/{bookingId}") + public BookingDtoResponse approve( + @RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId, + @PathVariable @Positive Long bookingId, + @RequestParam("approved") Boolean approved + ) { + return bookingService.approve(userId, bookingId, approved); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/{bookingId}") + public BookingDtoResponse get(@RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId, @PathVariable @Positive Long bookingId) { + return bookingService.get(userId, bookingId); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping + public List getAllBookingCurrentUser( + @RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId, + @RequestParam(name = "state", defaultValue = "ALL") BookingState state + ) { + return bookingService.getAllBookingCurrentUser(userId, state); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/owner") + public List getAllBookingAllItemCurrentUser(@RequestHeader(value = "X-Sharer-User-Id") @Positive Long userId, @RequestParam(name = "state", defaultValue = "ALL") BookingState state) { + return bookingService.getAllBookingAllItemCurrentUser(userId, state); + } + +} diff --git a/server/src/main/java/ru/practicum/shareit/booking/dto/BookingDtoRequestCreate.java b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingDtoRequestCreate.java new file mode 100644 index 0000000..a7d9f73 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingDtoRequestCreate.java @@ -0,0 +1,20 @@ +package ru.practicum.shareit.booking.dto; + +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +import java.time.LocalDateTime; + +public record BookingDtoRequestCreate( + @NotNull(message = "Необходимо указать идентификатор вещи") + @Positive(message = "Идентификатор вещи должен быть больше нуля") + Long itemId, + @NotNull(message = "Необходимо указать дату и время начала аренды") + @FutureOrPresent(message = "Нельзя указывать прошедшую дату и время начала аренды") + LocalDateTime start, + @FutureOrPresent(message = "Нельзя указывать прошедшую дату и время окончания аренды") + @NotNull(message = "Необходимо указать дату и время окончания аренды") + LocalDateTime end +) { +} diff --git a/server/src/main/java/ru/practicum/shareit/booking/dto/BookingDtoResponse.java b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingDtoResponse.java new file mode 100644 index 0000000..1c8aeae --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingDtoResponse.java @@ -0,0 +1,17 @@ +package ru.practicum.shareit.booking.dto; + +import ru.practicum.shareit.booking.model.Status; +import ru.practicum.shareit.item.dto.ItemDtoResponse; +import ru.practicum.shareit.user.dto.UserDtoResponse; + +import java.time.LocalDateTime; + +public record BookingDtoResponse( + Long id, + LocalDateTime start, + LocalDateTime end, + Status status, + ItemDtoResponse item, + UserDtoResponse booker +) { +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/booking/mapper/BookingMapper.java b/server/src/main/java/ru/practicum/shareit/booking/mapper/BookingMapper.java new file mode 100644 index 0000000..b57f2f0 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/mapper/BookingMapper.java @@ -0,0 +1,25 @@ +package ru.practicum.shareit.booking.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.ReportingPolicy; +import ru.practicum.shareit.booking.dto.BookingDtoRequestCreate; +import ru.practicum.shareit.booking.dto.BookingDtoResponse; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.util.List; + +@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = MappingConstants.ComponentModel.SPRING) +public interface BookingMapper { + @Mapping(target = "id", ignore = true) + @Mapping(target = "booker", source = "user") + @Mapping(target = "status", constant = "WAITING") + Booking toBookingDtoRequestCreate(BookingDtoRequestCreate bookingCreateDto, Item item, User user); + + BookingDtoResponse toBookingDtoResponse(Booking booking); + + List toListBookingDtoResponse(List bookings); +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/booking/model/Booking.java b/server/src/main/java/ru/practicum/shareit/booking/model/Booking.java new file mode 100644 index 0000000..4cc93d0 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -0,0 +1,36 @@ +package ru.practicum.shareit.booking.model; + +import jakarta.persistence.*; +import lombok.*; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Entity +public class Booking { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "start_time", nullable = false) + private LocalDateTime start; + + @Column(name = "end_time") + private LocalDateTime end; + + @ManyToOne(optional = false) + private Item item; + + @ManyToOne(optional = false) + private User booker; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Status status; +} diff --git a/server/src/main/java/ru/practicum/shareit/booking/model/BookingState.java b/server/src/main/java/ru/practicum/shareit/booking/model/BookingState.java new file mode 100644 index 0000000..a574273 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/model/BookingState.java @@ -0,0 +1,10 @@ +package ru.practicum.shareit.booking.model; + +public enum BookingState { + ALL, + CURRENT, + PAST, + FUTURE, + WAITING, + REJECTED +} diff --git a/server/src/main/java/ru/practicum/shareit/booking/model/Status.java b/server/src/main/java/ru/practicum/shareit/booking/model/Status.java new file mode 100644 index 0000000..f51353b --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/model/Status.java @@ -0,0 +1,8 @@ +package ru.practicum.shareit.booking.model; + +public enum Status { + WAITING, + APPROVED, + REJECTED, + CANCELED +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java b/server/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java new file mode 100644 index 0000000..d6e81c0 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java @@ -0,0 +1,34 @@ +package ru.practicum.shareit.booking.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.Status; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Optional; + +@Repository +public interface BookingRepository extends JpaRepository, JpaSpecificationExecutor { + + boolean existsByBookerIdAndItemIdAndStartBeforeAndStatusNotIn( + Long bookerId, + Long itemId, + LocalDateTime now, + Collection excluded + ); + + Optional findFirstByItemIdAndStartBeforeAndEndBeforeOrderByEndDesc( + Long itemId, + LocalDateTime startBefore, + LocalDateTime endBefore + ); + + Optional findFirstByItemIdAndStartAfterOrderByStartAsc( + Long itemId, + LocalDateTime now + ); + +} diff --git a/server/src/main/java/ru/practicum/shareit/booking/service/BookingService.java b/server/src/main/java/ru/practicum/shareit/booking/service/BookingService.java new file mode 100644 index 0000000..4146707 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/service/BookingService.java @@ -0,0 +1,104 @@ +package ru.practicum.shareit.booking.service; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.booking.dto.BookingDtoRequestCreate; +import ru.practicum.shareit.booking.dto.BookingDtoResponse; +import ru.practicum.shareit.booking.mapper.BookingMapper; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.BookingState; +import ru.practicum.shareit.booking.model.Status; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.exception.*; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.service.ItemService; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.service.UserService; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class BookingService { + private final BookingRepository bookingRepository; + private final BookingMapper bookingMapper; + private final UserService userService; + private final ItemService itemService; + + public BookingDtoResponse create(Long userId, BookingDtoRequestCreate booking) { + User user = userService.getUser(userId); + Item item = itemService.getItem(booking.itemId()); + if (!item.isAvailable()) throw new ExceptionBookingNotAvailable("Вещь не доступна для бронирования"); + Booking saved = bookingRepository.save(bookingMapper.toBookingDtoRequestCreate(booking, item, user)); + return bookingMapper.toBookingDtoResponse(saved); + } + + public BookingDtoResponse approve(Long userId, Long bookingId, Boolean approved) { + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new ExceptionBookingNotFound("Бронирования с идентификатором = " + bookingId + " не найдено")); + if (!booking.getItem().getOwner().getId().equals(userId)) + throw new ExceptionBookingNotOwner("Только владелец товара может одобрять бронирование"); + if (booking.getStatus() != Status.WAITING) + throw new ExceptionInvalidBookingState("Статус бронирования должен быть WAITING (Новое бронирование)"); + if (approved) booking.setStatus(Status.APPROVED); + else booking.setStatus(Status.REJECTED); + Booking saved = bookingRepository.save(booking); + return bookingMapper.toBookingDtoResponse(saved); + } + + public BookingDtoResponse get(Long requesterId, Long bookingId) { + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new ExceptionBookingNotFound("Бронирования с идентификатором = " + bookingId + " не найдено")); + boolean isBooker = booking.getBooker().getId().equals(requesterId); + boolean isOwner = booking.getItem().getOwner().getId().equals(requesterId); + if (!isBooker && !isOwner) + throw new ExceptionBookingForbiddenOperation("Отказано в доступе к бронированию"); + return bookingMapper.toBookingDtoResponse(booking); + } + + public List getAllBookingCurrentUser(Long userId, BookingState bookingState) { + userService.getUser(userId); + Specification specification = (root, query, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + predicates.add(criteriaBuilder.equal(root.get("booker").get("id"), userId)); + predicates.add(statePredicate(root, criteriaBuilder, bookingState, Instant.now())); + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }; + List result = bookingRepository.findAll(specification, Sort.by(Sort.Direction.DESC, "start")); + return bookingMapper.toListBookingDtoResponse(result); + } + + public List getAllBookingAllItemCurrentUser(Long userId, BookingState bookingState) { + userService.getUser(userId); + Specification specification = (root, query, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + predicates.add(criteriaBuilder.equal(root.get("item").get("owner").get("id"), userId)); + predicates.add(statePredicate(root, criteriaBuilder, bookingState, Instant.now())); + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }; + List result = bookingRepository.findAll(specification, Sort.by(Sort.Direction.DESC, "start")); + return bookingMapper.toListBookingDtoResponse(result); + } + + private static Predicate statePredicate(Root root, CriteriaBuilder criteriaBuilder, BookingState bookingState, Instant now) { + return switch (bookingState) { + case CURRENT -> criteriaBuilder.and( + criteriaBuilder.lessThanOrEqualTo(root.get("start"), now), + criteriaBuilder.greaterThanOrEqualTo(root.get("end"), now) + ); + case PAST -> criteriaBuilder.lessThan(root.get("end"), now); + case FUTURE -> criteriaBuilder.greaterThan(root.get("start"), now); + case WAITING -> criteriaBuilder.equal(root.get("status"), Status.WAITING); + case REJECTED -> criteriaBuilder.equal(root.get("status"), Status.REJECTED); + case ALL -> criteriaBuilder.conjunction(); + }; + } + +} diff --git a/server/src/main/java/ru/practicum/shareit/comment/dto/CommentDtoRequestCreate.java b/server/src/main/java/ru/practicum/shareit/comment/dto/CommentDtoRequestCreate.java new file mode 100644 index 0000000..991c544 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/comment/dto/CommentDtoRequestCreate.java @@ -0,0 +1,8 @@ +package ru.practicum.shareit.comment.dto; + +import jakarta.validation.constraints.NotBlank; + +public record CommentDtoRequestCreate( + @NotBlank String text +) { +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/comment/dto/CommentDtoResponse.java b/server/src/main/java/ru/practicum/shareit/comment/dto/CommentDtoResponse.java new file mode 100644 index 0000000..970dbe3 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/comment/dto/CommentDtoResponse.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.comment.dto; + +import java.time.LocalDateTime; + +public record CommentDtoResponse( + Long id, + String text, + String authorName, + LocalDateTime created +) { +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/comment/mapper/CommentMapper.java b/server/src/main/java/ru/practicum/shareit/comment/mapper/CommentMapper.java new file mode 100644 index 0000000..506d5ac --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/comment/mapper/CommentMapper.java @@ -0,0 +1,25 @@ +package ru.practicum.shareit.comment.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import ru.practicum.shareit.comment.dto.CommentDtoResponse; +import ru.practicum.shareit.comment.model.Comment; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.util.List; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface CommentMapper { + @Mapping(target = "id", ignore = true) + @Mapping(target = "item", source = "item") + @Mapping(target = "author", source = "author") + @Mapping(target = "text", source = "text") + Comment toCommentDtoRequestCreate(String text, Item item, User author); + + @Mapping(target = "authorName", source = "author.name") + CommentDtoResponse toCommentDtoResponse(Comment comment); + + List toListCommentDtoResponse(List comments); +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/comment/model/Comment.java b/server/src/main/java/ru/practicum/shareit/comment/model/Comment.java new file mode 100644 index 0000000..d85a11e --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/comment/model/Comment.java @@ -0,0 +1,42 @@ +package ru.practicum.shareit.comment.model; + +import jakarta.persistence.*; +import lombok.*; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Entity +@Table(name = "comments") +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "item_id") + private Item item; + + @ManyToOne(optional = false) + @JoinColumn(name = "author_id") + private User author; + + @Column(name = "text", nullable = false) + private String text; + + @Column(name = "created", nullable = false) + private LocalDateTime created; + + @PrePersist + void prePersist() { + if (created == null) created = LocalDateTime.now(); + } + +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/comment/repository/CommentRepository.java b/server/src/main/java/ru/practicum/shareit/comment/repository/CommentRepository.java new file mode 100644 index 0000000..c889a24 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/comment/repository/CommentRepository.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.comment.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.shareit.comment.model.Comment; + +public interface CommentRepository extends JpaRepository { +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/exception/ApplicationExceptionHandler.java b/server/src/main/java/ru/practicum/shareit/exception/ApplicationExceptionHandler.java new file mode 100644 index 0000000..cd08e5a --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exception/ApplicationExceptionHandler.java @@ -0,0 +1,48 @@ +package ru.practicum.shareit.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class ApplicationExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ProblemDetail handleUserNotFound(MethodArgumentNotValidException ex) { + ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage()); + problem.setTitle("Ошибка валидации."); + problem.setProperty("error", "Ошибка валидации."); + return problem; + } + + @ExceptionHandler(ExceptionItemNotFound.class) + public ProblemDetail handleUserNotFound(ExceptionItemNotFound ex) { + ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()); + problem.setTitle("Запрошенная вещь не найдена."); + return problem; + } + + @ExceptionHandler(ExceptionUserEmailExists.class) + public ProblemDetail handleUserNotFound(ExceptionUserEmailExists ex) { + ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage()); + problem.setTitle("Электронная почта уже занята другим пользователем."); + return problem; + } + + @ExceptionHandler(ExceptionUserNotFound.class) + public ProblemDetail handleUserNotFound(ExceptionUserNotFound ex) { + ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()); + problem.setTitle("Запрошенный пользователь не найден."); + return problem; + } + + @ExceptionHandler(Exception.class) + public ProblemDetail handleUserNotFound(Exception ex) { + ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage()); + problem.setTitle("Внутренняя ошибка сервера."); + return problem; + } + +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/exception/ExceptionBookingForbiddenOperation.java b/server/src/main/java/ru/practicum/shareit/exception/ExceptionBookingForbiddenOperation.java new file mode 100644 index 0000000..d9d35eb --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exception/ExceptionBookingForbiddenOperation.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionBookingForbiddenOperation extends RuntimeException { + public ExceptionBookingForbiddenOperation(String message) { + super(message); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotAvailable.java b/server/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotAvailable.java new file mode 100644 index 0000000..d3923e4 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotAvailable.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionBookingNotAvailable extends RuntimeException { + public ExceptionBookingNotAvailable(String message) { + super(message); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotFound.java b/server/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotFound.java new file mode 100644 index 0000000..8e91d8b --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotFound.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionBookingNotFound extends RuntimeException { + public ExceptionBookingNotFound(String message) { + super(message); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotOwner.java b/server/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotOwner.java new file mode 100644 index 0000000..aceced3 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exception/ExceptionBookingNotOwner.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionBookingNotOwner extends RuntimeException { + public ExceptionBookingNotOwner(String message) { + super(message); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/exception/ExceptionInvalidBookingState.java b/server/src/main/java/ru/practicum/shareit/exception/ExceptionInvalidBookingState.java new file mode 100644 index 0000000..be9a6f4 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exception/ExceptionInvalidBookingState.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionInvalidBookingState extends RuntimeException { + public ExceptionInvalidBookingState(String message) { + super(message); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/exception/ExceptionItemNotFound.java b/server/src/main/java/ru/practicum/shareit/exception/ExceptionItemNotFound.java new file mode 100644 index 0000000..5667113 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exception/ExceptionItemNotFound.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionItemNotFound extends RuntimeException { + public ExceptionItemNotFound(String message) { + super(message); + } +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/exception/ExceptionItemRequestNotFound.java b/server/src/main/java/ru/practicum/shareit/exception/ExceptionItemRequestNotFound.java new file mode 100644 index 0000000..b251df6 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exception/ExceptionItemRequestNotFound.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionItemRequestNotFound extends RuntimeException { + public ExceptionItemRequestNotFound(String message) { + super(message); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/exception/ExceptionNotActiveBooking.java b/server/src/main/java/ru/practicum/shareit/exception/ExceptionNotActiveBooking.java new file mode 100644 index 0000000..6d695ff --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exception/ExceptionNotActiveBooking.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionNotActiveBooking extends RuntimeException { + public ExceptionNotActiveBooking(String message) { + super(message); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/exception/ExceptionUserEmailExists.java b/server/src/main/java/ru/practicum/shareit/exception/ExceptionUserEmailExists.java new file mode 100644 index 0000000..93f8745 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exception/ExceptionUserEmailExists.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionUserEmailExists extends RuntimeException { + public ExceptionUserEmailExists(String message) { + super(message); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/exception/ExceptionUserNotFound.java b/server/src/main/java/ru/practicum/shareit/exception/ExceptionUserNotFound.java new file mode 100644 index 0000000..40d3fbd --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/exception/ExceptionUserNotFound.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ExceptionUserNotFound extends RuntimeException { + public ExceptionUserNotFound(String message) { + super(message); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/item/controller/ItemController.java b/server/src/main/java/ru/practicum/shareit/item/controller/ItemController.java new file mode 100644 index 0000000..3feec57 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/controller/ItemController.java @@ -0,0 +1,85 @@ +package ru.practicum.shareit.item.controller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.comment.dto.CommentDtoRequestCreate; +import ru.practicum.shareit.comment.dto.CommentDtoResponse; +import ru.practicum.shareit.item.dto.ItemDtoRequestCreate; +import ru.practicum.shareit.item.dto.ItemDtoRequestUpdate; +import ru.practicum.shareit.item.dto.ItemDtoResponse; +import ru.practicum.shareit.item.dto.ItemDtoResponseWithBookings; +import ru.practicum.shareit.item.service.ItemService; + +import java.util.List; + +@RequiredArgsConstructor +@Validated +@RestController +@RequestMapping("/items") +public class ItemController { + private final ItemService itemService; + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public ItemDtoResponse create( + @RequestHeader("X-Sharer-User-Id") Long userId, + @Validated @RequestBody ItemDtoRequestCreate item + ) { + return itemService.create(userId, item); + } + + @ResponseStatus(HttpStatus.OK) + @PatchMapping("/{itemId}") + public ItemDtoResponse update( + @PathVariable Long itemId, + @RequestHeader("X-Sharer-User-Id") Long userId, + @Validated @RequestBody ItemDtoRequestUpdate item + ) { + return itemService.update(itemId, userId, item); + } + + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping("/{itemId}") + public void delete(@PathVariable Long itemId) { + itemService.delete(itemId); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/{itemId}") + public ItemDtoResponseWithBookings getById( + @RequestHeader("X-Sharer-User-Id") Long requesterId, + @PathVariable Long itemId + ) { + return itemService.getById(requesterId, itemId); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping + public List getByUserId(@RequestHeader("X-Sharer-User-Id") Long userId) { + return itemService.getByUserId(userId); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/search") + public List search(@RequestParam String text) { + if (!StringUtils.hasText(text)) + return List.of(); + return itemService.search(text); + } + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping("/{itemId}/comment") + public CommentDtoResponse addComment( + @RequestHeader("X-Sharer-User-Id") @Positive Long userId, + @PathVariable @Positive Long itemId, + @Valid @RequestBody CommentDtoRequestCreate comment + ) { + return itemService.createComment(userId, itemId, comment); + } + +} diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/ItemDtoRequestCreate.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDtoRequestCreate.java new file mode 100644 index 0000000..fcf8b8f --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDtoRequestCreate.java @@ -0,0 +1,15 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record ItemDtoRequestCreate( + @NotBlank(message = "Название вещи не может быть пустым") + String name, + @NotBlank(message = "Описание вещи не может быть пустым") + String description, + @NotNull(message = "Необходимо указать статус доступа к аренде") + Boolean available, + Long requestId +) { +} diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/ItemDtoRequestId.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDtoRequestId.java new file mode 100644 index 0000000..bc4ace7 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDtoRequestId.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.item.dto; + +public record ItemDtoRequestId(Long id) { +} diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/ItemDtoRequestUpdate.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDtoRequestUpdate.java new file mode 100644 index 0000000..3608caf --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDtoRequestUpdate.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.item.dto; + +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; + +public record ItemDtoRequestUpdate( + String name, + String description, + Boolean available, + ItemRequestDtoRequestCreate request +) { +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/ItemDtoResponse.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDtoResponse.java new file mode 100644 index 0000000..489f49e --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDtoResponse.java @@ -0,0 +1,14 @@ +package ru.practicum.shareit.item.dto; + +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoResponse; + +public record ItemDtoResponse( + Long id, + String name, + String description, + boolean available, + UserDtoResponse userDtoResponse, + ItemRequestDtoRequestCreate request +) { +} diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/ItemDtoResponseWithBookings.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDtoResponseWithBookings.java new file mode 100644 index 0000000..b01e50c --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDtoResponseWithBookings.java @@ -0,0 +1,20 @@ +package ru.practicum.shareit.item.dto; + +import ru.practicum.shareit.booking.dto.BookingDtoResponse; +import ru.practicum.shareit.comment.dto.CommentDtoResponse; +import ru.practicum.shareit.user.dto.UserDtoResponse; + +import java.util.List; + +public record ItemDtoResponseWithBookings( + Long id, + String name, + String description, + boolean available, + UserDtoResponse userResponseDto, + ItemDtoRequestId request, + BookingDtoResponse lastBooking, + BookingDtoResponse nextBooking, + List comments +) { +} diff --git a/server/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java b/server/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java new file mode 100644 index 0000000..b253451 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/mapper/ItemMapper.java @@ -0,0 +1,40 @@ +package ru.practicum.shareit.item.mapper; + +import org.mapstruct.*; +import ru.practicum.shareit.booking.mapper.BookingMapper; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.comment.mapper.CommentMapper; +import ru.practicum.shareit.item.dto.ItemDtoRequestCreate; +import ru.practicum.shareit.item.dto.ItemDtoRequestUpdate; +import ru.practicum.shareit.item.dto.ItemDtoResponse; +import ru.practicum.shareit.item.dto.ItemDtoResponseWithBookings; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.request.mapper.ItemRequestMapper; +import ru.practicum.shareit.user.mapper.UserMapper; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.model.User; + +import java.util.List; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, uses = {UserMapper.class, BookingMapper.class, + CommentMapper.class, ItemRequestMapper.class}) +public interface ItemMapper { + @Mapping(target = "id", ignore = true) + @Mapping(target = "name", source = "item.name") + @Mapping(target = "description", source = "item.description") + @Mapping(target = "available", source = "item.available") + @Mapping(target = "owner", source = "owner") + @Mapping(target = "request", source = "request") + Item toItemDtoRequestCreate(ItemDtoRequestCreate item, User owner, ItemRequest request); + + @Mapping(target = "request", ignore = true) + ItemDtoResponse toItemDtoResponse(Item item); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + void itemDtoRequestUpdate(ItemDtoRequestUpdate itemDtoRequestUpdate, @MappingTarget Item item); + + List toListItemDtoResponse(List items); + + @Mapping(target = "id", source = "item.id") + ItemDtoResponseWithBookings toItemDtoResponseWithBookings(Item item, Booking last, Booking next); +} diff --git a/server/src/main/java/ru/practicum/shareit/item/model/Item.java b/server/src/main/java/ru/practicum/shareit/item/model/Item.java new file mode 100644 index 0000000..2242574 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -0,0 +1,35 @@ +package ru.practicum.shareit.item.model; + +import jakarta.persistence.*; +import lombok.*; +import ru.practicum.shareit.comment.model.Comment; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.model.User; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Entity +public class Item { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(nullable = false) + private String name; + @Column(nullable = false) + private String description; + @Column(nullable = false) + private boolean available; + @ManyToOne(optional = false) + private User owner; + @ManyToOne() + private ItemRequest request; + @OneToMany(mappedBy = "item", fetch = FetchType.LAZY) + @Builder.Default + private List comments = new ArrayList<>(); +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java b/server/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java new file mode 100644 index 0000000..3bfb053 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java @@ -0,0 +1,23 @@ +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.*; + +public interface ItemRepository extends JpaRepository { + + @Query("SELECT i FROM Item i LEFT JOIN FETCH i.comments WHERE i.id = :id") + Optional findByIdWithComments(@Param("id") Long id); + + List findByOwnerId(Long ownerId); + + List findItemsByNameContainingIgnoreCaseAndAvailable(String name, boolean available); + + List findAllByOwnerIdOrderByIdAsc(Long ownerId); + + List findAllByRequest_idOrderByIdAsc(Long requestId); + +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/item/service/ItemService.java b/server/src/main/java/ru/practicum/shareit/item/service/ItemService.java new file mode 100644 index 0000000..c4dc18f --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/service/ItemService.java @@ -0,0 +1,115 @@ +package ru.practicum.shareit.item.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.model.Status; +import ru.practicum.shareit.booking.repository.BookingRepository; +import ru.practicum.shareit.comment.dto.CommentDtoRequestCreate; +import ru.practicum.shareit.comment.dto.CommentDtoResponse; +import ru.practicum.shareit.comment.mapper.CommentMapper; +import ru.practicum.shareit.comment.model.Comment; +import ru.practicum.shareit.comment.repository.CommentRepository; +import ru.practicum.shareit.exception.ExceptionItemNotFound; +import ru.practicum.shareit.exception.ExceptionNotActiveBooking; +import ru.practicum.shareit.item.dto.ItemDtoRequestCreate; +import ru.practicum.shareit.item.dto.ItemDtoRequestUpdate; +import ru.practicum.shareit.item.dto.ItemDtoResponse; +import ru.practicum.shareit.item.dto.ItemDtoResponseWithBookings; +import ru.practicum.shareit.item.mapper.ItemMapper; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.request.service.ItemRequestService; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.service.UserService; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +@Service +@RequiredArgsConstructor +public class ItemService { + private final ItemRepository itemRepository; + private final ItemMapper itemMapper; + private final UserService userService; + private final BookingRepository bookingRepository; + private final CommentRepository commentRepository; + private final CommentMapper commentMapper; + private final ItemRequestService itemRequestService; + + public ItemDtoResponse create(Long userId, ItemDtoRequestCreate itemDtoRequestCreate) { + User user = userService.getUser(userId); + ItemRequest itemRequest = null; + if (itemDtoRequestCreate.requestId() != null) + itemRequest = itemRequestService.getItemRequest(itemDtoRequestCreate.requestId()); + + Item item = itemMapper.toItemDtoRequestCreate(itemDtoRequestCreate, user, itemRequest); + return itemMapper.toItemDtoResponse(itemRepository.save(item)); + } + + public ItemDtoResponse update(Long itemId, Long userId, ItemDtoRequestUpdate itemNew) { + Item itemOld = getItem(itemId); + if (!Objects.equals(itemOld.getOwner().getId(), userId)) { + throw new ExceptionItemNotFound("Вещь с идентификатором (" + itemId + ") не найдена для пользователя с идентификатором (" + userId + ")."); + } + itemMapper.itemDtoRequestUpdate(itemNew, itemOld); + itemRepository.save(itemOld); + return itemMapper.toItemDtoResponse(itemOld); + } + + public void delete(Long itemId) { + getItem(itemId); + itemRepository.deleteById(itemId); + } + + public ItemDtoResponseWithBookings getById(Long requesterId, Long itemId) { + Item item = itemRepository.findByIdWithComments(itemId) + .orElseThrow(() -> new ExceptionItemNotFound("Вещь с идентификатором (" + itemId + ") не найдена")); + Booking last = null; + Booking next = null; + if (item.getOwner() != null && item.getOwner().getId().equals(requesterId)) { + LocalDateTime now = LocalDateTime.now(); + last = bookingRepository + .findFirstByItemIdAndStartBeforeAndEndBeforeOrderByEndDesc(itemId, now, now) + .orElse(null); + next = bookingRepository + .findFirstByItemIdAndStartAfterOrderByStartAsc(itemId, now) + .orElse(null); + } + return itemMapper.toItemDtoResponseWithBookings(item, last, next); + } + + public List getByUserId(Long userId) { + userService.getUser(userId); + return itemMapper.toListItemDtoResponse(itemRepository.findByOwnerId(userId)); + } + + public List search(String text) { + return itemMapper.toListItemDtoResponse(itemRepository.findItemsByNameContainingIgnoreCaseAndAvailable(text, true)); + } + + public CommentDtoResponse createComment(Long userId, Long itemId, CommentDtoRequestCreate comment) { + boolean isNotActiveBooking = bookingRepository.existsByBookerIdAndItemIdAndStartBeforeAndStatusNotIn( + userId, + itemId, + LocalDateTime.now(), + List.of(Status.REJECTED, Status.WAITING, Status.CANCELED) + ); + if (!isNotActiveBooking) + throw new ExceptionNotActiveBooking("У пользователя с идентификатором " + userId + + " не активно бронирование вещи с идентификатором " + itemId); + User user = userService.getUser(userId); + Item item = getItem(itemId); + Comment entity = commentMapper.toCommentDtoRequestCreate(comment.text(), item, user); + Comment saved = commentRepository.save(entity); + return commentMapper.toCommentDtoResponse(saved); + } + + public Item getItem(Long itemId) { + return itemRepository.findById(itemId) + .orElseThrow(() -> new ExceptionItemNotFound("Вещь с идентификатором (" + itemId + ") не найдена.")); + } + +} diff --git a/server/src/main/java/ru/practicum/shareit/request/contoller/ItemRequestController.java b/server/src/main/java/ru/practicum/shareit/request/contoller/ItemRequestController.java new file mode 100644 index 0000000..a3221c3 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/contoller/ItemRequestController.java @@ -0,0 +1,55 @@ +package ru.practicum.shareit.request.contoller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.request.dto.ItemRequestDtoListResponse; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; +import ru.practicum.shareit.request.dto.ItemRequestDtoResponse; +import ru.practicum.shareit.request.service.ItemRequestService; + +import java.util.List; + +@RestController +@RequestMapping("/requests") +@RequiredArgsConstructor +@Validated +public class ItemRequestController { + private final ItemRequestService itemRequestService; + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public ItemRequestDtoResponse create( + @RequestHeader(value = "X-Sharer-User-Id") Long userId, + @RequestBody ItemRequestDtoRequestCreate itemRequest + ) { + return itemRequestService.create(userId, itemRequest); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping + public List getAllRequestByUserId(@RequestHeader(value = "X-Sharer-User-Id") Long userId) { + return itemRequestService.getAllRequestByUserId(userId); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/{requestId}") + public ItemRequestDtoListResponse getRequestById( + @PathVariable long requestId, + @RequestHeader(value = "X-Sharer-User-Id") Long userId + ) { + return itemRequestService.getRequestById(requestId, userId); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/all") + public List getAll( + @RequestParam(defaultValue = "0", required = false) int from, + @RequestParam(defaultValue = "20", required = false) int quantity, + @RequestHeader(value = "X-Sharer-User-Id") Long userId + ) { + return itemRequestService.getAll(from, quantity, userId); + } + +} diff --git a/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDtoListResponse.java b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDtoListResponse.java new file mode 100644 index 0000000..26f06b7 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDtoListResponse.java @@ -0,0 +1,26 @@ +package ru.practicum.shareit.request.dto; + +import lombok.Builder; +import ru.practicum.shareit.item.dto.ItemDtoResponse; +import ru.practicum.shareit.request.model.ItemRequest; + +import java.time.LocalDateTime; +import java.util.List; + +@Builder +public record ItemRequestDtoListResponse( + Long id, + String description, + LocalDateTime created, + List items +) { + + public static ItemRequestDtoListResponse create(ItemRequest itemRequest, List items) { + return ItemRequestDtoListResponse.builder() + .id(itemRequest.getId()) + .description(itemRequest.getDescription()) + .created(itemRequest.getCreated()) + .items(items) + .build(); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDtoRequestCreate.java b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDtoRequestCreate.java new file mode 100644 index 0000000..93bf404 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDtoRequestCreate.java @@ -0,0 +1,12 @@ +package ru.practicum.shareit.request.dto; + +import jakarta.validation.constraints.NotBlank; + +import java.time.LocalDateTime; + +public record ItemRequestDtoRequestCreate( + @NotBlank(message = "Описание вещи не может быть пустым") + String description, + LocalDateTime created +) { +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDtoRequestId.java b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDtoRequestId.java new file mode 100644 index 0000000..f22123f --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDtoRequestId.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.request.dto; + +public record ItemRequestDtoRequestId(Long requestId) { +} diff --git a/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDtoResponse.java b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDtoResponse.java new file mode 100644 index 0000000..f8738b5 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDtoResponse.java @@ -0,0 +1,10 @@ +package ru.practicum.shareit.request.dto; + +import java.time.LocalDateTime; + +public record ItemRequestDtoResponse( + Long id, + String description, + LocalDateTime created +) { +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/request/mapper/ItemRequestMapper.java b/server/src/main/java/ru/practicum/shareit/request/mapper/ItemRequestMapper.java new file mode 100644 index 0000000..d03ba64 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/mapper/ItemRequestMapper.java @@ -0,0 +1,23 @@ +package ru.practicum.shareit.request.mapper; + +import org.mapstruct.*; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; +import ru.practicum.shareit.request.dto.ItemRequestDtoResponse; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.model.User; + +import java.util.List; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface ItemRequestMapper { + @Mapping(target = "id", ignore = true) + @Mapping(target = "description", source = "itemRequest.description") + @Mapping(target = "created", source = "itemRequest.created") + @Mapping(target = "requestor", source = "requestor") + ItemRequest toItemRequestDtoRequestCreate(ItemRequestDtoRequestCreate itemRequest, User requestor); + + ItemRequestDtoResponse toItemRequestDtoResponse(ItemRequest itemRequest); + + List toListItemRequestDtoResponse(List itemsRequest); + +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java b/server/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java new file mode 100644 index 0000000..ff26af0 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java @@ -0,0 +1,33 @@ +package ru.practicum.shareit.request.model; + +import jakarta.persistence.*; +import lombok.*; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Entity +public class ItemRequest { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String description; + + @ManyToOne(optional = false) + private User requestor; + + @Column(nullable = false) + private LocalDateTime created; + + @PrePersist + void prePersist() { + if (created == null) created = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java b/server/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java new file mode 100644 index 0000000..e27f032 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java @@ -0,0 +1,17 @@ +package ru.practicum.shareit.request.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.shareit.request.model.ItemRequest; + +import java.util.Collection; +import java.util.List; + +public interface ItemRequestRepository extends JpaRepository { + + List findAllByRequestor_idOrderByCreatedAsc(Long userId); + + Page findAllByRequestor_idNotIn(Collection ownerId, Pageable pageable); + +} diff --git a/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java new file mode 100644 index 0000000..0e4cab9 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java @@ -0,0 +1,76 @@ +package ru.practicum.shareit.request.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.exception.ExceptionItemRequestNotFound; +import ru.practicum.shareit.item.dto.ItemDtoResponse; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.request.dto.ItemRequestDtoListResponse; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; +import ru.practicum.shareit.request.dto.ItemRequestDtoResponse; +import ru.practicum.shareit.request.mapper.ItemRequestMapper; +import ru.practicum.shareit.item.mapper.ItemMapper; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.request.repository.ItemRequestRepository; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.service.UserService; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ItemRequestService { + private final ItemRequestRepository itemRequestRepository; + private final ItemRepository itemRepository; + private final ItemRequestMapper itemRequestMapper; + private final UserService userService; + private final ItemMapper itemMapper; + + public ItemRequestDtoResponse create(Long userId, ItemRequestDtoRequestCreate itemRequestDtoRequestCreate) { + User user = userService.getUser(userId); + ItemRequest itemRequest = itemRequestMapper.toItemRequestDtoRequestCreate(itemRequestDtoRequestCreate, user); + return itemRequestMapper.toItemRequestDtoResponse(itemRequestRepository.save(itemRequest)); + } + + public List getAllRequestByUserId(Long userId) { + userService.getUser(userId); + if (itemRequestRepository.findAllByRequestor_idOrderByCreatedAsc(userId).isEmpty()) + return List.of(); + + return itemRequestRepository.findAllByRequestor_idOrderByCreatedAsc(userId) + .stream() + .map(ir -> ItemRequestDtoListResponse.create(ir, itemMapper.toListItemDtoResponse( + itemRepository.findAllByOwnerIdOrderByIdAsc(ir.getRequestor().getId()) + ))) + .collect(Collectors.toList()); + } + + public ItemRequestDtoListResponse getRequestById(Long requestId, Long userId) { + userService.getUser(userId); + ItemRequest itemRequest = getItemRequest(requestId); + List items = itemRepository.findAllByRequest_idOrderByIdAsc(requestId); + List item = itemMapper.toListItemDtoResponse(items); + return ItemRequestDtoListResponse.create(itemRequest, item); + } + + public List getAll(int from, int quantity, long userId) { + userService.getUser(userId); + Pageable pageable = PageRequest.of(from, quantity, Sort.by(Sort.Direction.DESC, "created")); + return itemRequestRepository.findAllByRequestor_idNotIn(List.of(userId), pageable).getContent() + .stream() + .map(ir -> ItemRequestDtoListResponse.create(ir, itemMapper.toListItemDtoResponse( + itemRepository.findAllByOwnerIdOrderByIdAsc(ir.getRequestor().getId())))) + .collect(Collectors.toList()); + } + + public ItemRequest getItemRequest(Long id) { + return itemRequestRepository.findById(id) + .orElseThrow(() -> new ExceptionItemRequestNotFound("Запрос вещи с идентификатором (" + id + ") не найден.")); + } + +} diff --git a/server/src/main/java/ru/practicum/shareit/user/controller/UserController.java b/server/src/main/java/ru/practicum/shareit/user/controller/UserController.java new file mode 100644 index 0000000..7bc5c79 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/controller/UserController.java @@ -0,0 +1,42 @@ +package ru.practicum.shareit.user.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.user.dto.UserDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoRequestUpdate; +import ru.practicum.shareit.user.dto.UserDtoResponse; +import ru.practicum.shareit.user.service.UserService; + +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/users") +public class UserController { + private final UserService userService; + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public UserDtoResponse create(@Valid @RequestBody UserDtoRequestCreate user) { + return userService.create(user); + } + + @ResponseStatus(HttpStatus.OK) + @PatchMapping("/{id}") + public UserDtoResponse update(@PathVariable Long id, @Valid @RequestBody UserDtoRequestUpdate user) { + return userService.update(id, user); + } + + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping("/{id}") + public void delete(@PathVariable Long id) { + userService.delete(id); + } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/{id}") + public UserDtoResponse get(@PathVariable Long id) { + return userService.get(id); + } + +} diff --git a/server/src/main/java/ru/practicum/shareit/user/dto/UserDtoRequestCreate.java b/server/src/main/java/ru/practicum/shareit/user/dto/UserDtoRequestCreate.java new file mode 100644 index 0000000..353a2bb --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/dto/UserDtoRequestCreate.java @@ -0,0 +1,13 @@ +package ru.practicum.shareit.user.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record UserDtoRequestCreate( + @NotBlank(message = "Имя пользователя не может быть пустым") + String name, + @NotBlank(message = "Электронная почта пользователя не может быть пустая") + @Email + String email +) { +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/user/dto/UserDtoRequestUpdate.java b/server/src/main/java/ru/practicum/shareit/user/dto/UserDtoRequestUpdate.java new file mode 100644 index 0000000..43b178a --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/dto/UserDtoRequestUpdate.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.user.dto; + +public record UserDtoRequestUpdate(String name, String email) { +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/user/dto/UserDtoResponse.java b/server/src/main/java/ru/practicum/shareit/user/dto/UserDtoResponse.java new file mode 100644 index 0000000..850fb0c --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/dto/UserDtoResponse.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.user.dto; + +public record UserDtoResponse(Long id, String name, String email) { +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/user/mapper/UserMapper.java b/server/src/main/java/ru/practicum/shareit/user/mapper/UserMapper.java new file mode 100644 index 0000000..c1c0f47 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/mapper/UserMapper.java @@ -0,0 +1,17 @@ +package ru.practicum.shareit.user.mapper; + +import org.mapstruct.*; +import ru.practicum.shareit.user.dto.UserDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoRequestUpdate; +import ru.practicum.shareit.user.dto.UserDtoResponse; +import ru.practicum.shareit.user.model.User; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface UserMapper { + UserDtoResponse toUserDtoResponse(User user); + + User toUserDtoRequestCreate(UserDtoRequestCreate userDtoRequestCreate); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + void userDtoRequestUpdate(UserDtoRequestUpdate userDtoRequestUpdate, @MappingTarget User user); +} diff --git a/server/src/main/java/ru/practicum/shareit/user/model/User.java b/server/src/main/java/ru/practicum/shareit/user/model/User.java new file mode 100644 index 0000000..b65f2c2 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/model/User.java @@ -0,0 +1,23 @@ +package ru.practicum.shareit.user.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Entity +@Table(name = "users") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(nullable = false) + private String name; + @Column(nullable = false, unique = true) + private String email; +} diff --git a/server/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java b/server/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java new file mode 100644 index 0000000..680f11a --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java @@ -0,0 +1,9 @@ +package ru.practicum.shareit.user.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.practicum.shareit.user.model.User; + +@Repository +public interface UserRepository extends JpaRepository { +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/user/service/UserService.java b/server/src/main/java/ru/practicum/shareit/user/service/UserService.java new file mode 100644 index 0000000..de7a257 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/service/UserService.java @@ -0,0 +1,44 @@ +package ru.practicum.shareit.user.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.exception.ExceptionUserNotFound; +import ru.practicum.shareit.user.dto.UserDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoRequestUpdate; +import ru.practicum.shareit.user.dto.UserDtoResponse; +import ru.practicum.shareit.user.mapper.UserMapper; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +@Service +@RequiredArgsConstructor +public class UserService { + private final UserRepository userRepository; + private final UserMapper userMapper; + + public UserDtoResponse create(UserDtoRequestCreate user) { + return userMapper.toUserDtoResponse(userRepository.save(userMapper.toUserDtoRequestCreate(user))); + } + + public UserDtoResponse update(Long id, UserDtoRequestUpdate userNew) { + User userOld = getUser(id); + userMapper.userDtoRequestUpdate(userNew, userOld); + userRepository.save(userOld); + return userMapper.toUserDtoResponse(userOld); + } + + public void delete(Long id) { + getUser(id); + userRepository.deleteById(id); + } + + public UserDtoResponse get(Long id) { + return userMapper.toUserDtoResponse(getUser(id)); + } + + public User getUser(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new ExceptionUserNotFound("Пользователь с идентификатором (" + id + ") не найден.")); + } + +} \ No newline at end of file diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties new file mode 100644 index 0000000..c931eee --- /dev/null +++ b/server/src/main/resources/application.properties @@ -0,0 +1,18 @@ +server.port=9090 + +spring.jpa.hibernate.ddl-auto=none +spring.jpa.properties.hibernate.format_sql=true +spring.sql.init.mode=always + +#--- +spring.datasource.driverClassName=org.postgresql.Driver +spring.datasource.url=jdbc:postgresql://localhost:5432/shareit +spring.datasource.username=dbuser +spring.datasource.password=12345 +#--- + +spring.config.activate.on-profile=test +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.url=jdbc:h2:mem:shareit +spring.datasource.username=shareit +spring.datasource.password=shareit \ No newline at end of file diff --git a/server/src/main/resources/schema.sql b/server/src/main/resources/schema.sql new file mode 100644 index 0000000..99cd58f --- /dev/null +++ b/server/src/main/resources/schema.sql @@ -0,0 +1,43 @@ +DROP TABLE IF EXISTS booking CASCADE; +DROP TABLE IF EXISTS item CASCADE; +DROP TABLE IF EXISTS item_request CASCADE; +DROP TABLE IF EXISTS users CASCADE; +DROP TABLE IF EXISTS comments CASCADE; +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_request( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + description VARCHAR(255) NOT NULL, + requestor_id BIGINT NOT NULL REFERENCES users (id), + created TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE TABLE IF NOT EXISTS item( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description VARCHAR(255) NOT NULL, + available BOOLEAN NOT NULL, + owner_id BIGINT NOT NULL REFERENCES users (id), + request_id BIGINT REFERENCES item_request (id) +); + +CREATE TABLE IF NOT EXISTS booking( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + start_time TIMESTAMP WITHOUT TIME ZONE NOT NULL, + end_time TIMESTAMP WITHOUT TIME ZONE, + item_id BIGINT NOT NULL REFERENCES item (id), + booker_id BIGINT NOT NULL REFERENCES users (id), + status VARCHAR(255) NOT NULL +); + +CREATE TABLE IF NOT EXISTS comments( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + text VARCHAR(255) NOT NULL, + item_id BIGINT NOT NULL REFERENCES item (id), + author_id BIGINT NOT NULL REFERENCES users (id), + created TIMESTAMP WITHOUT TIME ZONE NOT NULL +); \ No newline at end of file diff --git a/src/test/java/ru/practicum/shareit/ShareItTests.java b/server/src/test/java/ru/practicum/shareit/ShareItTests.java similarity index 79% rename from src/test/java/ru/practicum/shareit/ShareItTests.java rename to server/src/test/java/ru/practicum/shareit/ShareItTests.java index 4d79052..1d13d7f 100644 --- a/src/test/java/ru/practicum/shareit/ShareItTests.java +++ b/server/src/test/java/ru/practicum/shareit/ShareItTests.java @@ -6,8 +6,8 @@ @SpringBootTest class ShareItTests { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } -} +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/booking/TestBookingController.java b/server/src/test/java/ru/practicum/shareit/booking/TestBookingController.java new file mode 100644 index 0000000..e67b138 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/TestBookingController.java @@ -0,0 +1,143 @@ +package ru.practicum.shareit.booking; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.booking.contoller.BookingController; +import ru.practicum.shareit.booking.dto.BookingDtoRequestCreate; +import ru.practicum.shareit.booking.dto.BookingDtoResponse; +import ru.practicum.shareit.booking.model.BookingState; +import ru.practicum.shareit.booking.model.Status; +import ru.practicum.shareit.booking.service.BookingService; +import ru.practicum.shareit.item.dto.ItemDtoResponse; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoResponse; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@WebMvcTest(controllers = BookingController.class) +public class TestBookingController { + @Autowired + ObjectMapper mapper; + @Autowired + private MockMvc mvc; + @MockBean + BookingService bookingService; + private final BookingDtoRequestCreate bookingDtoRequestCreate = new BookingDtoRequestCreate( + 1L, + LocalDateTime.now().plusHours(1), + LocalDateTime.now().plusDays(1) + ); + private final UserDtoResponse userDtoResponse = new UserDtoResponse( + 1L, + "Пользователь №1", + "mail@mail.ru" + ); + private final ItemRequestDtoRequestCreate itemRequestDtoRequestCreate = new ItemRequestDtoRequestCreate( + "Описание запроса на вещь №1", + LocalDateTime.now() + ); + private final ItemDtoResponse itemDtoResponse = new ItemDtoResponse( + 1L, + "Название вещи", + "Описание вещи", + true, + userDtoResponse, + itemRequestDtoRequestCreate + ); + private final BookingDtoResponse bookingDtoResponse = new BookingDtoResponse( + 1L, + LocalDateTime.now().plusHours(1), + LocalDateTime.now().plusDays(1), + Status.WAITING, + itemDtoResponse, + userDtoResponse + ); + + @Test + void createBooking() throws Exception { + Mockito + .when(bookingService.create(1L, bookingDtoRequestCreate)) + .thenReturn(bookingDtoResponse); + bookingService.create(1L, bookingDtoRequestCreate); + mvc.perform(post("/bookings") + .header("X-Sharer-User-Id", 1L) + .content(mapper.writeValueAsString(bookingDtoRequestCreate)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()); + } + + @Test + void approveBooking() throws Exception { + when(bookingService.approve(1L, 1L, true)) + .thenReturn(bookingDtoResponse); + mvc.perform(patch("/bookings/1?approved=true") + .content(mapper.writeValueAsString(bookingDtoRequestCreate)) + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(bookingDtoResponse.id()), Long.class)) + .andExpect(jsonPath("$.status", is(bookingDtoResponse.status().toString()))); + } + + @Test + void getBooking() throws Exception { + when(bookingService.get(1L, 1L)) + .thenReturn(bookingDtoResponse); + mvc.perform(get("/bookings/1") + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(bookingDtoResponse.id()), Long.class)) + .andExpect(jsonPath("$.status", is(bookingDtoResponse.status().toString()))); + } + + @Test + void getAllBookingCurrentUser() throws Exception { + when(bookingService.getAllBookingCurrentUser(1L, BookingState.ALL)) + .thenReturn(List.of(bookingDtoResponse)); + mvc.perform(get("/bookings") + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id", is(bookingDtoResponse.id()), Long.class)) + .andExpect(jsonPath("$[0].status", is(bookingDtoResponse.status().toString()))); + } + + @Test + void getAllBookingAllItemCurrentUser() throws Exception { + when(bookingService.getAllBookingAllItemCurrentUser(1L, BookingState.ALL)) + .thenReturn(List.of(bookingDtoResponse)); + mvc.perform(get("/bookings/owner") + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id", is(bookingDtoResponse.id()), Long.class)) + .andExpect(jsonPath("$[0].status", is(bookingDtoResponse.status().toString()))); + } + +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/booking/TestBookingDto.java b/server/src/test/java/ru/practicum/shareit/booking/TestBookingDto.java new file mode 100644 index 0000000..22a8345 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/TestBookingDto.java @@ -0,0 +1,80 @@ +package ru.practicum.shareit.booking; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import ru.practicum.shareit.booking.dto.BookingDtoRequestCreate; +import ru.practicum.shareit.booking.dto.BookingDtoResponse; +import ru.practicum.shareit.booking.model.Status; +import ru.practicum.shareit.item.dto.ItemDtoResponse; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoResponse; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +public class TestBookingDto { + @Autowired + private JacksonTester jacksonTester1; + private final BookingDtoRequestCreate bookingDtoRequestCreate = new BookingDtoRequestCreate( + 1L, + LocalDateTime.of(2025, 10, 16, 12, 12), + LocalDateTime.of(2025, 11, 16, 12, 12) + ); + + @Autowired + private JacksonTester jacksonTester2; + private final UserDtoResponse userDtoResponse = new UserDtoResponse( + 1L, + "Пользователь №1", + "mail@mail.ru" + ); + private final ItemRequestDtoRequestCreate itemRequestDtoRequestCreate = new ItemRequestDtoRequestCreate( + "Описание запроса на вещь №1", + LocalDateTime.now() + ); + private final ItemDtoResponse itemDtoResponse = new ItemDtoResponse( + 1L, + "Название вещи", + "Описание вещи", + true, + userDtoResponse, + itemRequestDtoRequestCreate + ); + private final BookingDtoResponse bookingDtoResponse = new BookingDtoResponse( + 1L, + LocalDateTime.of(2025, 10, 16, 12, 12), + LocalDateTime.of(2025, 11, 16, 12, 12), + Status.WAITING, + itemDtoResponse, + userDtoResponse + ); + + @Test + void bookingDtoRequestCreate() throws Exception { + JsonContent jsonContent = jacksonTester1.write(bookingDtoRequestCreate); + assertThat(jsonContent).extractingJsonPathNumberValue("$.itemId").isEqualTo(1); + assertThat(jsonContent).extractingJsonPathStringValue("$.start").isEqualTo("2025-10-16T12:12:00"); + assertThat(jsonContent).extractingJsonPathStringValue("$.end").isEqualTo("2025-11-16T12:12:00"); + } + + @Test + void bookingDtoResponse() throws Exception { + JsonContent jsonContent = jacksonTester2.write(bookingDtoResponse); + assertThat(jsonContent).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(jsonContent).extractingJsonPathStringValue("$.start").isEqualTo("2025-10-16T12:12:00"); + assertThat(jsonContent).extractingJsonPathStringValue("$.end").isEqualTo("2025-11-16T12:12:00"); + assertThat(jsonContent).extractingJsonPathStringValue("$.status").isEqualTo("WAITING"); + assertThat(jsonContent).extractingJsonPathNumberValue("$.item.id").isEqualTo(1); + assertThat(jsonContent).extractingJsonPathStringValue("$.item.name").isEqualTo("Название вещи"); + assertThat(jsonContent).extractingJsonPathStringValue("$.item.description").isEqualTo("Описание вещи"); + assertThat(jsonContent).extractingJsonPathNumberValue("$.booker.id").isEqualTo(1); + assertThat(jsonContent).extractingJsonPathStringValue("$.booker.name").isEqualTo("Пользователь №1"); + assertThat(jsonContent).extractingJsonPathStringValue("$.booker.email").isEqualTo("mail@mail.ru"); + } + +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/booking/TestBookingService.java b/server/src/test/java/ru/practicum/shareit/booking/TestBookingService.java new file mode 100644 index 0000000..3d93f6d --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/TestBookingService.java @@ -0,0 +1,70 @@ +package ru.practicum.shareit.booking; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.booking.dto.BookingDtoRequestCreate; +import ru.practicum.shareit.booking.dto.BookingDtoResponse; +import ru.practicum.shareit.booking.model.BookingState; +import ru.practicum.shareit.booking.model.Status; +import ru.practicum.shareit.booking.service.BookingService; +import ru.practicum.shareit.exception.ExceptionBookingForbiddenOperation; +import ru.practicum.shareit.exception.ExceptionBookingNotFound; +import ru.practicum.shareit.exception.ExceptionBookingNotOwner; +import ru.practicum.shareit.exception.ExceptionUserNotFound; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.LocalDateTime; +import java.util.List; + +@Transactional +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@SpringBootTest +@Sql(scripts = "/data_service.sql") +class TestBookingService { + private final BookingService bookingService; + private final BookingDtoRequestCreate bookingDtoRequestCreate = new BookingDtoRequestCreate( + 1L, + LocalDateTime.parse("2025-09-01T01:00:00"), + LocalDateTime.parse("2025-12-01T01:00:00") + ); + + @Test + void createBooking() { + BookingDtoResponse bookingDtoResponse = bookingService.create(1L, bookingDtoRequestCreate); + assertThat(bookingDtoResponse.start(), equalTo(bookingDtoRequestCreate.start())); + assertThat(bookingDtoResponse.end(), equalTo(bookingDtoRequestCreate.end())); + assertThrows(ExceptionUserNotFound.class, () -> bookingService.create(100L, bookingDtoRequestCreate)); + } + + @Test + void approveBooking() { + BookingDtoResponse bookingDtoResponse = bookingService.approve(2L, 2L, true); + assertThat(bookingDtoResponse.status(), equalTo(Status.APPROVED)); + assertThrows(ExceptionBookingNotOwner.class, () -> bookingService.approve(300L, 2L, true)); + assertThrows(ExceptionBookingNotFound.class, () -> bookingService.approve(3L, 200L, true)); + } + + @Test + void getBooking() { + BookingDtoResponse bookingDtoResponse = bookingService.get(2L, 5L); + assertThat(bookingDtoResponse.status(), equalTo(Status.CANCELED)); + assertThrows(ExceptionBookingNotFound.class, () -> bookingService.get(1L, 100L)); + assertThrows(ExceptionBookingForbiddenOperation.class, () -> bookingService.get(100L, 1L)); + } + + @Test + void getAllBookingCurrentUser() { + List bookingDtoResponse = bookingService.getAllBookingCurrentUser(2L, BookingState.ALL); + assertThat(bookingDtoResponse.getFirst().status(), equalTo(Status.CANCELED)); + assertThat(bookingDtoResponse.size(), equalTo(2)); + assertThrows(ExceptionUserNotFound.class, () -> bookingService.getAllBookingCurrentUser(100L, BookingState.ALL)); + } + +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/comment/TestCommentDto.java b/server/src/test/java/ru/practicum/shareit/comment/TestCommentDto.java new file mode 100644 index 0000000..a4ef17c --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/comment/TestCommentDto.java @@ -0,0 +1,47 @@ +package ru.practicum.shareit.comment; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import ru.practicum.shareit.comment.dto.CommentDtoRequestCreate; +import ru.practicum.shareit.comment.dto.CommentDtoResponse; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +public class TestCommentDto { + @Autowired + private JacksonTester jacksonTester1; + private final CommentDtoRequestCreate commentDtoRequestCreate = new CommentDtoRequestCreate( + "Текст комментария" + ); + + @Autowired + private JacksonTester jacksonTester2; + private final CommentDtoResponse commentDtoResponse = new CommentDtoResponse( + 1L, + "Текст комментария", + "Автор комментария", + LocalDateTime.of(2025, 10, 16, 12, 12) + ); + + @Test + void commentDtoRequestCreate() throws Exception { + JsonContent jsonContent = jacksonTester1.write(commentDtoRequestCreate); + assertThat(jsonContent).extractingJsonPathStringValue("$.text").isEqualTo("Текст комментария"); + } + + @Test + void commentDtoResponse() throws Exception { + JsonContent jsonContent = jacksonTester2.write(commentDtoResponse); + assertThat(jsonContent).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(jsonContent).extractingJsonPathStringValue("$.text").isEqualTo("Текст комментария"); + assertThat(jsonContent).extractingJsonPathStringValue("$.authorName").isEqualTo("Автор комментария"); + assertThat(jsonContent).extractingJsonPathStringValue("$.created").isEqualTo("2025-10-16T12:12:00"); + } + +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/TestItemController.java b/server/src/test/java/ru/practicum/shareit/item/TestItemController.java new file mode 100644 index 0000000..aad393f --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/TestItemController.java @@ -0,0 +1,172 @@ +package ru.practicum.shareit.item; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.comment.dto.CommentDtoRequestCreate; +import ru.practicum.shareit.comment.dto.CommentDtoResponse; +import ru.practicum.shareit.item.controller.ItemController; +import ru.practicum.shareit.item.dto.*; +import ru.practicum.shareit.item.service.ItemService; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoResponse; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(controllers = ItemController.class) +public class TestItemController { + @Autowired + ObjectMapper mapper; + @Autowired + private MockMvc mvc; + @MockBean + ItemService itemService; + private final UserDtoResponse userDtoResponse = new UserDtoResponse( + 1L, + "Пользователь №1", + "mail@mail.ru" + ); + private final ItemRequestDtoRequestCreate itemRequestDtoRequestCreate = new ItemRequestDtoRequestCreate( + "Описание запроса на вещь №1", + LocalDateTime.now() + ); + private final ItemDtoResponse itemDtoResponse = new ItemDtoResponse( + 1L, + "Название вещи", + "Описание вещи", + true, + userDtoResponse, + itemRequestDtoRequestCreate + ); + private final ItemDtoRequestCreate itemDtoRequestCreate = new ItemDtoRequestCreate( + "Название вещи", + "Описание вещи", + true, + 2L + ); + private final ItemDtoRequestUpdate itemDtoRequestUpdate = new ItemDtoRequestUpdate( + "Название вещи", + "Описание вещи", + false, + itemRequestDtoRequestCreate + ); + private final CommentDtoRequestCreate commentDtoRequestCreate = new CommentDtoRequestCreate( + "Текст комментария" + ); + private final ItemDtoResponseWithBookings itemDtoResponseWithBookings = new ItemDtoResponseWithBookings( + 1L, + "Название вещи", + "Описание вещи", + false, + userDtoResponse, + null, + null, + null, + null + ); + private final CommentDtoResponse commentDtoResponse = new CommentDtoResponse( + 1L, + "Текст комментария", + "Автор комментария", + LocalDateTime.now() + ); + + @Test + void createItem() throws Exception { + when(itemService.create(1L, itemDtoRequestCreate)) + .thenReturn(itemDtoResponse); + mvc.perform(post("/items") + .content(mapper.writeValueAsString(itemDtoRequestCreate)) + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()); + } + + @Test + void updateItem() throws Exception { + when(itemService.update(1L, 1L, itemDtoRequestUpdate)) + .thenReturn(itemDtoResponse); + mvc.perform(patch("/items/1") + .content(mapper.writeValueAsString(itemDtoRequestUpdate)) + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name", is(itemDtoResponse.name()))) + .andExpect(jsonPath("$.description", is(itemDtoResponse.description()))); + } + + @Test + void deleteItem() throws Exception { + mvc.perform(delete("/items/1").characterEncoding(StandardCharsets.UTF_8)) + .andExpect(status().is2xxSuccessful()); + } + + @Test + void getItemById() throws Exception { + when(itemService.getById(1L, 1L)) + .thenReturn(itemDtoResponseWithBookings); + mvc.perform(get("/items/1") + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name", is(itemDtoRequestUpdate.name()))) + .andExpect(jsonPath("$.description", is(itemDtoRequestUpdate.description()))); + } + + @Test + void getItemByUserId() throws Exception { + when(itemService.getByUserId(1L)) + .thenReturn(List.of(itemDtoResponse)); + mvc.perform(get("/items") + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name", is(itemDtoRequestUpdate.name()))) + .andExpect(jsonPath("$[0].description", is(itemDtoRequestUpdate.description()))); + + } + + @Test + void searchItem() throws Exception { + when(itemService.search("Название вещи")) + .thenReturn(List.of(itemDtoResponse)); + mvc.perform(get("/items/search?text=Название вещи").characterEncoding(StandardCharsets.UTF_8)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name", is(itemDtoRequestUpdate.name()))) + .andExpect(jsonPath("$[0].description", is(itemDtoRequestUpdate.description()))); + } + + @Test + void createComment() throws Exception { + when(itemService.createComment(1L, 1L, commentDtoRequestCreate)) + .thenReturn(commentDtoResponse); + mvc.perform(post("/items/1/comment") + .content(mapper.writeValueAsString(commentDtoRequestCreate)) + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()); + } + +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/TestItemDto.java b/server/src/test/java/ru/practicum/shareit/item/TestItemDto.java new file mode 100644 index 0000000..13d4f07 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/TestItemDto.java @@ -0,0 +1,155 @@ +package ru.practicum.shareit.item; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import ru.practicum.shareit.booking.dto.BookingDtoResponse; +import ru.practicum.shareit.booking.model.Status; +import ru.practicum.shareit.item.dto.*; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoResponse; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +public class TestItemDto { + @Autowired + private JacksonTester jacksonTester1; + private final ItemDtoRequestCreate itemDtoRequestCreate = new ItemDtoRequestCreate( + "Название вещи", + "Описание вещи", + true, + 1L + ); + + @Autowired + private JacksonTester jacksonTester2; + private final ItemDtoRequestId itemDtoRequestId = new ItemDtoRequestId( + 1L + ); + + @Autowired + private JacksonTester jacksonTester3; + private final ItemRequestDtoRequestCreate itemRequestDtoRequestCreate = new ItemRequestDtoRequestCreate( + "Описание вещи", + LocalDateTime.of(2025, 10, 16, 12, 12) + ); + private final ItemDtoRequestUpdate itemDtoRequestUpdate = new ItemDtoRequestUpdate( + "Название вещи", + "Описание вещи", + true, + itemRequestDtoRequestCreate + ); + + @Autowired + private JacksonTester jacksonTester4; + private final UserDtoResponse userDtoResponse = new UserDtoResponse( + 1L, + "Пользователь №1", + "mail@mail.ru" + ); + private final ItemDtoResponse itemDtoResponse = new ItemDtoResponse( + 1L, + "Название вещи", + "Описание вещи", + true, + userDtoResponse, + itemRequestDtoRequestCreate + ); + + @Autowired + private JacksonTester jacksonTester5; + private final BookingDtoResponse bookingDtoResponse1 = new BookingDtoResponse( + 1L, + LocalDateTime.of(2025, 10, 16, 12, 12), + LocalDateTime.of(2025, 11, 16, 12, 12), + Status.WAITING, + null, + null + ); + private final BookingDtoResponse bookingDtoResponse2 = new BookingDtoResponse( + 1L, + LocalDateTime.of(2025, 10, 16, 12, 12), + LocalDateTime.of(2025, 11, 16, 12, 12), + Status.WAITING, + null, + null + ); + private final ItemDtoResponseWithBookings itemDtoResponseWithBookings = new ItemDtoResponseWithBookings( + 1L, + "Название вещи", + "Описание вещи", + true, + userDtoResponse, + null, + bookingDtoResponse1, + bookingDtoResponse2, + null + ); + + @Test + void itemDtoRequestCreate() throws Exception { + JsonContent jsonContent = jacksonTester1.write(itemDtoRequestCreate); + assertThat(jsonContent).extractingJsonPathStringValue("$.name").isEqualTo("Название вещи"); + assertThat(jsonContent).extractingJsonPathStringValue("$.description").isEqualTo("Описание вещи"); + assertThat(jsonContent).extractingJsonPathNumberValue("$.requestId").isEqualTo(1); + assertThat(jsonContent).extractingJsonPathBooleanValue("$.available").isTrue(); + } + + @Test + void itemDtoRequestId() throws Exception { + JsonContent jsonContent = jacksonTester2.write(itemDtoRequestId); + assertThat(jsonContent).extractingJsonPathNumberValue("$.id").isEqualTo(1); + } + + @Test + void itemDtoRequestUpdate() throws Exception { + JsonContent jsonContent = jacksonTester3.write(itemDtoRequestUpdate); + assertThat(jsonContent).extractingJsonPathStringValue("$.name").isEqualTo("Название вещи"); + assertThat(jsonContent).extractingJsonPathStringValue("$.description").isEqualTo("Описание вещи"); + assertThat(jsonContent).extractingJsonPathStringValue("$.request.description").isEqualTo("Описание вещи"); + assertThat(jsonContent).extractingJsonPathStringValue("$.request.created").isEqualTo("2025-10-16T12:12:00"); + assertThat(jsonContent).extractingJsonPathBooleanValue("$.available").isTrue(); + } + + @Test + void itemDtoResponse() throws Exception { + JsonContent jsonContent = jacksonTester4.write(itemDtoResponse); + assertThat(jsonContent).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(jsonContent).extractingJsonPathStringValue("$.name").isEqualTo("Название вещи"); + assertThat(jsonContent).extractingJsonPathStringValue("$.description").isEqualTo("Описание вещи"); + assertThat(jsonContent).extractingJsonPathBooleanValue("$.available").isTrue(); + assertThat(jsonContent).extractingJsonPathNumberValue("$.userDtoResponse.id").isEqualTo(1); + assertThat(jsonContent).extractingJsonPathStringValue("$.userDtoResponse.name").isEqualTo("Пользователь №1"); + assertThat(jsonContent).extractingJsonPathStringValue("$.userDtoResponse.email").isEqualTo("mail@mail.ru"); + assertThat(jsonContent).extractingJsonPathStringValue("$.request.description").isEqualTo("Описание вещи"); + assertThat(jsonContent).extractingJsonPathStringValue("$.request.created").isEqualTo("2025-10-16T12:12:00"); + } + + @Test + void itemDtoResponseWithBookings() throws Exception { + JsonContent jsonContent = jacksonTester5.write(itemDtoResponseWithBookings); + assertThat(jsonContent).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(jsonContent).extractingJsonPathStringValue("$.name").isEqualTo("Название вещи"); + assertThat(jsonContent).extractingJsonPathStringValue("$.description").isEqualTo("Описание вещи"); + assertThat(jsonContent).extractingJsonPathBooleanValue("$.available").isTrue(); + assertThat(jsonContent).extractingJsonPathNumberValue("$.userResponseDto.id").isEqualTo(1); + assertThat(jsonContent).extractingJsonPathStringValue("$.userResponseDto.name").isEqualTo("Пользователь №1"); + assertThat(jsonContent).extractingJsonPathStringValue("$.userResponseDto.email").isEqualTo("mail@mail.ru"); + + assertThat(jsonContent).extractingJsonPathNumberValue("$.lastBooking.id").isEqualTo(1); + assertThat(jsonContent).extractingJsonPathStringValue("$.lastBooking.start").isEqualTo("2025-10-16T12:12:00"); + assertThat(jsonContent).extractingJsonPathStringValue("$.lastBooking.end").isEqualTo("2025-11-16T12:12:00"); + assertThat(jsonContent).extractingJsonPathStringValue("$.lastBooking.status").isEqualTo("WAITING"); + + assertThat(jsonContent).extractingJsonPathNumberValue("$.nextBooking.id").isEqualTo(1); + assertThat(jsonContent).extractingJsonPathStringValue("$.nextBooking.start").isEqualTo("2025-10-16T12:12:00"); + assertThat(jsonContent).extractingJsonPathStringValue("$.nextBooking.end").isEqualTo("2025-11-16T12:12:00"); + assertThat(jsonContent).extractingJsonPathStringValue("$.nextBooking.status").isEqualTo("WAITING"); + } + +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/TestItemService.java b/server/src/test/java/ru/practicum/shareit/item/TestItemService.java new file mode 100644 index 0000000..a6d7844 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/TestItemService.java @@ -0,0 +1,86 @@ +package ru.practicum.shareit.item; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import ru.practicum.shareit.comment.dto.CommentDtoRequestCreate; +import ru.practicum.shareit.comment.dto.CommentDtoResponse; +import ru.practicum.shareit.exception.ExceptionItemNotFound; +import ru.practicum.shareit.exception.ExceptionNotActiveBooking; +import ru.practicum.shareit.item.dto.*; +import ru.practicum.shareit.item.service.ItemService; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Transactional +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@SpringBootTest +@Sql(scripts = "/data_service.sql") +class TestItemService { + private final ItemService itemService; + private final ItemDtoRequestCreate itemDtoRequestCreate = new ItemDtoRequestCreate( + "Название вещи", + "Описание вещи", + true, + 2L + ); + private final ItemRequestDtoRequestCreate itemRequestDtoRequestCreate = new ItemRequestDtoRequestCreate( + "Описание запроса на вещь №5", + LocalDateTime.now() + ); + private final ItemDtoRequestUpdate itemDtoRequestUpdate = new ItemDtoRequestUpdate( + "Название вещи №5 новое", + "Описание вещи №5 новое", + false, + itemRequestDtoRequestCreate + ); + + @Test + void createItem() { + ItemDtoResponse itemDtoResponse = itemService.create(1L, itemDtoRequestCreate); + assertThat(itemDtoResponse.name(), equalTo(itemDtoRequestCreate.name())); + assertThat(itemDtoResponse.description(), equalTo(itemDtoRequestCreate.description())); + assertThat(itemDtoResponse.available(), equalTo(itemDtoRequestCreate.available())); + } + + @Test + void updateItem() { + ItemDtoResponse itemUpdate = itemService.update(5L, 5L, itemDtoRequestUpdate); + assertThat(itemUpdate.name(), equalTo(itemDtoRequestUpdate.name())); + assertThat(itemUpdate.description(), equalTo(itemDtoRequestUpdate.description())); + assertThat(itemUpdate.available(), equalTo(itemDtoRequestUpdate.available())); + assertThrows(ExceptionItemNotFound.class, () -> itemService.update(100L, 5L, itemDtoRequestUpdate)); + } + + @Test + void deleteItem() { + itemService.delete(5L); + assertThrows(ExceptionItemNotFound.class, () -> itemService.getItem(5L)); + } + + @Test + void searchItemsByName() { + List items = itemService.search("Название вещи №4"); + assertThat(items.size(), equalTo(1)); + assertThat(items.getFirst().name(), equalTo("Название вещи №4")); + } + + @Test + void createComment() { + CommentDtoRequestCreate commentDtoRequestCreate = new CommentDtoRequestCreate( + "Очень объемное финальное задание получается..." + ); + CommentDtoResponse commentDtoResponse = itemService.createComment(2L, 1L, commentDtoRequestCreate); + assertThat(commentDtoResponse.text(), equalTo(commentDtoRequestCreate.text())); + assertThrows(ExceptionNotActiveBooking.class, () -> itemService.createComment(100L, 100L, commentDtoRequestCreate)); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/request/TestItemRequestController.java b/server/src/test/java/ru/practicum/shareit/request/TestItemRequestController.java new file mode 100644 index 0000000..bbc16e4 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/TestItemRequestController.java @@ -0,0 +1,102 @@ +package ru.practicum.shareit.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.request.contoller.ItemRequestController; +import ru.practicum.shareit.request.dto.ItemRequestDtoListResponse; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; +import ru.practicum.shareit.request.dto.ItemRequestDtoResponse; +import ru.practicum.shareit.request.service.ItemRequestService; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@WebMvcTest(controllers = ItemRequestController.class) +public class TestItemRequestController { + @Autowired + ObjectMapper mapper; + @Autowired + private MockMvc mvc; + @MockBean + ItemRequestService itemRequestService; + private final ItemRequestDtoRequestCreate itemRequestDtoRequestCreate = new ItemRequestDtoRequestCreate( + "Описание запроса на вещь №1", + LocalDateTime.now() + ); + private final ItemRequestDtoResponse itemRequestDtoResponse = new ItemRequestDtoResponse( + 1L, + "Описание запроса на вещь №1", + LocalDateTime.now() + ); + private final ItemRequestDtoListResponse itemRequestDtoListResponse = new ItemRequestDtoListResponse( + 1L, + "Описание запроса на вещь №1", + LocalDateTime.now(), + null + ); + + @Test + void createItemRequest() throws Exception { + when(itemRequestService.create(1L, itemRequestDtoRequestCreate)) + .thenReturn(itemRequestDtoResponse); + mvc.perform(post("/requests") + .content(mapper.writeValueAsString(itemRequestDtoRequestCreate)) + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.description", is(itemRequestDtoResponse.description()))); + } + + @Test + void getAllItemRequestByUserId() throws Exception { + when(itemRequestService.getAllRequestByUserId(1L)) + .thenReturn(List.of(itemRequestDtoListResponse)); + mvc.perform(get("/requests") + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].description", is(itemRequestDtoRequestCreate.description()))); + } + + @Test + void getAllItemRequestById() throws Exception { + when(itemRequestService.getRequestById(1L, 1L)) + .thenReturn(itemRequestDtoListResponse); + mvc.perform(get("/requests/1") + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + void getAllItemRequest() throws Exception { + when(itemRequestService.getAll(0, 20, 1L)) + .thenReturn(List.of(itemRequestDtoListResponse)); + mvc.perform(get("/requests/all") + .header("X-Sharer-User-Id", 1L) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/request/TestItemRequestDto.java b/server/src/test/java/ru/practicum/shareit/request/TestItemRequestDto.java new file mode 100644 index 0000000..27890d1 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/TestItemRequestDto.java @@ -0,0 +1,60 @@ +package ru.practicum.shareit.request; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestId; +import ru.practicum.shareit.request.dto.ItemRequestDtoResponse; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +public class TestItemRequestDto { + @Autowired + private JacksonTester jacksonTester1; + private final ItemRequestDtoRequestCreate itemRequestDtoRequestCreate = new ItemRequestDtoRequestCreate( + "Описание вещи", + LocalDateTime.of(2025, 10, 16, 12, 12) + ); + + @Autowired + private JacksonTester jacksonTester2; + private final ItemRequestDtoRequestId itemRequestDtoRequestId = new ItemRequestDtoRequestId( + 1L + ); + + @Autowired + private JacksonTester jacksonTester3; + private final ItemRequestDtoResponse itemRequestDtoResponse = new ItemRequestDtoResponse( + 1L, + "Описание вещи", + LocalDateTime.of(2025, 10, 16, 12, 12) + ); + + @Test + void itemRequestDtoRequestCreate() throws Exception { + JsonContent jsonContent = jacksonTester1.write(itemRequestDtoRequestCreate); + assertThat(jsonContent).extractingJsonPathStringValue("$.description").isEqualTo("Описание вещи"); + assertThat(jsonContent).extractingJsonPathStringValue("$.created").isEqualTo("2025-10-16T12:12:00"); + } + + @Test + void itemRequestDtoRequestId() throws Exception { + JsonContent jsonContent = jacksonTester2.write(itemRequestDtoRequestId); + assertThat(jsonContent).extractingJsonPathNumberValue("$.requestId").isEqualTo(1); + } + + @Test + void itemRequestDtoResponse() throws Exception { + JsonContent jsonContent = jacksonTester3.write(itemRequestDtoResponse); + assertThat(jsonContent).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(jsonContent).extractingJsonPathStringValue("$.description").isEqualTo("Описание вещи"); + assertThat(jsonContent).extractingJsonPathStringValue("$.created").isEqualTo("2025-10-16T12:12:00"); + } + +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/request/TestItemRequestService.java b/server/src/test/java/ru/practicum/shareit/request/TestItemRequestService.java new file mode 100644 index 0000000..b1ebba0 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/TestItemRequestService.java @@ -0,0 +1,55 @@ +package ru.practicum.shareit.request; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import ru.practicum.shareit.exception.ExceptionUserNotFound; +import ru.practicum.shareit.request.dto.ItemRequestDtoListResponse; +import ru.practicum.shareit.request.dto.ItemRequestDtoRequestCreate; +import ru.practicum.shareit.request.dto.ItemRequestDtoResponse; +import ru.practicum.shareit.request.service.ItemRequestService; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Transactional +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@SpringBootTest +@Sql(scripts = "/data_service.sql") +class TestItemRequestService { + private final ItemRequestService itemRequestService; + private final ItemRequestDtoRequestCreate itemRequestDtoRequestCreate = new ItemRequestDtoRequestCreate( + "Новый запрос на поиск вещи", + LocalDateTime.now() + ); + + @Test + void createItemRequest() { + ItemRequestDtoResponse itemRequestDtoResponse = itemRequestService.create(1L, itemRequestDtoRequestCreate); + assertThat(itemRequestDtoRequestCreate.description(), equalTo(itemRequestDtoResponse.description())); + assertThrows(ExceptionUserNotFound.class, () -> + itemRequestService.create(100L, itemRequestDtoRequestCreate)); + } + + @Test + void getAllItemRequestByUserId() { + List listItemRequest = itemRequestService.getAllRequestByUserId(5L); + assertThat(listItemRequest.size(), equalTo(1)); + assertThat(listItemRequest.getFirst().id(), equalTo(5L)); + assertThrows(ExceptionUserNotFound.class, () -> + itemRequestService.getAllRequestByUserId(100L)); + } + + @Test + void getAllItemRequest() { + List listItemRequest = itemRequestService.getAll(0, 20, 4L); + assertThat(listItemRequest.size(), equalTo(4)); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/TestUserController.java b/server/src/test/java/ru/practicum/shareit/user/TestUserController.java new file mode 100644 index 0000000..ce40b38 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/TestUserController.java @@ -0,0 +1,93 @@ +package ru.practicum.shareit.user; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.user.controller.UserController; +import ru.practicum.shareit.user.dto.UserDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoRequestUpdate; +import ru.practicum.shareit.user.dto.UserDtoResponse; +import ru.practicum.shareit.user.service.UserService; + +import java.nio.charset.StandardCharsets; + +import static org.mockito.Mockito.when; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(controllers = UserController.class) +class TestUserController { + @Autowired + ObjectMapper mapper; + @Autowired + private MockMvc mvc; + @MockBean + UserService userService; + private final UserDtoResponse userDtoResponse = new UserDtoResponse( + 1L, + "Пользователь №1", + "mail@mail.ru" + ); + private final UserDtoRequestCreate userDtoRequestCreate = new UserDtoRequestCreate( + "Пользователь №1", + "mail@mail.ru" + ); + private final UserDtoRequestUpdate userDtoRequestUpdate = new UserDtoRequestUpdate( + "Пользователь №1 обновлен", + "mail@mail.ru" + ); + + @Test + void createUser() throws Exception { + when(userService.create(userDtoRequestCreate)) + .thenReturn(userDtoResponse); + mvc.perform(post("/users") + .content(mapper.writeValueAsString(userDtoRequestCreate)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()); + } + + @Test + void updateUser() throws Exception { + when(userService.update(1L, userDtoRequestUpdate)) + .thenReturn(userDtoResponse); + mvc.perform(patch("/users/1") + .content(mapper.writeValueAsString(userDtoRequestUpdate)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name", is(userDtoResponse.name()))) + .andExpect(jsonPath("$.email", is(userDtoResponse.email()))); + } + + @Test + void deleteUser() throws Exception { + mvc.perform(delete("/users/1") + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()); + } + + @Test + void getUser() throws Exception { + when(userService.get(1L)) + .thenReturn(userDtoResponse); + mvc.perform(get("/users/1") + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name", is(userDtoResponse.name()))) + .andExpect(jsonPath("$.email", is(userDtoResponse.email()))); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/TestUserDto.java b/server/src/test/java/ru/practicum/shareit/user/TestUserDto.java new file mode 100644 index 0000000..fff8baa --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/TestUserDto.java @@ -0,0 +1,60 @@ +package ru.practicum.shareit.user; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import ru.practicum.shareit.user.dto.UserDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoRequestUpdate; +import ru.practicum.shareit.user.dto.UserDtoResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +public class TestUserDto { + @Autowired + private JacksonTester jacksonTester1; + private final UserDtoRequestCreate userDtoRequestCreate = new UserDtoRequestCreate( + "Пользователь №1", + "mail@mail.ru" + ); + + @Autowired + private JacksonTester jacksonTester2; + private final UserDtoRequestUpdate userDtoRequestUpdate = new UserDtoRequestUpdate( + "Пользователь №1", + "mail@mail.ru" + ); + + @Autowired + private JacksonTester jacksonTester3; + private final UserDtoResponse userDtoResponse = new UserDtoResponse( + 1L, + "Пользователь №1", + "mail@mail.ru" + ); + + @Test + void userDtoRequestCreate() throws Exception { + JsonContent jsonContent = jacksonTester1.write(userDtoRequestCreate); + assertThat(jsonContent).extractingJsonPathStringValue("$.name").isEqualTo("Пользователь №1"); + assertThat(jsonContent).extractingJsonPathStringValue("$.email").isEqualTo("mail@mail.ru"); + } + + @Test + void userDtoRequestUpdate() throws Exception { + JsonContent jsonContent = jacksonTester2.write(userDtoRequestUpdate); + assertThat(jsonContent).extractingJsonPathStringValue("$.name").isEqualTo("Пользователь №1"); + assertThat(jsonContent).extractingJsonPathStringValue("$.email").isEqualTo("mail@mail.ru"); + } + + @Test + void userDtoResponse() throws Exception { + JsonContent jsonContent = jacksonTester3.write(userDtoResponse); + assertThat(jsonContent).extractingJsonPathStringValue("$.name").isEqualTo("Пользователь №1"); + assertThat(jsonContent).extractingJsonPathStringValue("$.email").isEqualTo("mail@mail.ru"); + assertThat(jsonContent).extractingJsonPathNumberValue("$.id").isEqualTo(1); + } + +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/TestUserRepository.java b/server/src/test/java/ru/practicum/shareit/user/TestUserRepository.java new file mode 100644 index 0000000..646edd8 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/TestUserRepository.java @@ -0,0 +1,48 @@ +package ru.practicum.shareit.user; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.annotation.Rollback; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +public class TestUserRepository { + private final User user = User.builder() + .name("Имя пользователя") + .email("mail@mail.ru") + .build(); + @Autowired + private UserRepository userRepository; + + @Test + @Rollback + void createUser() { + User current = userRepository.save(user); + assertThat(current).isEqualTo(user); + } + + @Test + @Rollback + void deleteUser() { + User current = userRepository.save(user); + userRepository.delete(current); + Optional searchUser = userRepository.findById(1L); + Assertions.assertFalse(searchUser.isPresent()); + } + + @Test + @Rollback + void findUser() { + User current = userRepository.save(user); + Optional searchUser = userRepository.findById(1L); + assertThat(current).isEqualTo(searchUser.get()); + } + +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/TestUserService.java b/server/src/test/java/ru/practicum/shareit/user/TestUserService.java new file mode 100644 index 0000000..632e5b2 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/TestUserService.java @@ -0,0 +1,60 @@ +package ru.practicum.shareit.user; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import ru.practicum.shareit.exception.ExceptionUserNotFound; +import ru.practicum.shareit.user.dto.UserDtoRequestCreate; +import ru.practicum.shareit.user.dto.UserDtoRequestUpdate; +import ru.practicum.shareit.user.dto.UserDtoResponse; +import ru.practicum.shareit.user.service.UserService; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Transactional +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@SpringBootTest +@Sql(scripts = "/data_service.sql") +class TestUserService { + private final UserService userService; + UserDtoRequestCreate userDtoRequestCreate = new UserDtoRequestCreate( + "Пользователь №6", + "mail6@mail.ru" + ); + + @Test + void createUser() { + UserDtoResponse user = userService.create(userDtoRequestCreate); + UserDtoResponse getUser = userService.get(user.id()); + assertThat(user, equalTo(getUser)); + } + + @Test + void updateUser() { + UserDtoRequestUpdate userUpdate = new UserDtoRequestUpdate( + "Пользователь №1 новый", + "mail1new@mail.ru" + ); + UserDtoResponse userNew = userService.update(1L, userUpdate); + assertThat(userNew.name(), equalTo("Пользователь №1 новый")); + assertThat(userNew.email(), equalTo("mail1new@mail.ru")); + } + + @Test + void getUser() { + UserDtoResponse userTwo = userService.get(2L); + assertThat(userTwo.name(), equalTo("Пользователь №2")); + assertThat(userTwo.email(), equalTo("mail2@mail.ru")); + } + + @Test + void deleteUser() { + userService.delete(5L); + assertThrows(ExceptionUserNotFound.class, () -> userService.get(5L)); + } +} \ No newline at end of file diff --git a/server/src/test/resources/data_service.sql b/server/src/test/resources/data_service.sql new file mode 100644 index 0000000..bcebd21 --- /dev/null +++ b/server/src/test/resources/data_service.sql @@ -0,0 +1,46 @@ +DELETE FROM comments; +DELETE FROM booking; +DELETE FROM item; +DELETE FROM item_request; +DELETE FROM users; + +ALTER TABLE comments ALTER COLUMN id RESTART WITH 1; +ALTER TABLE booking ALTER COLUMN id RESTART WITH 1; +ALTER TABLE item ALTER COLUMN id RESTART WITH 1; +ALTER TABLE item_request ALTER COLUMN id RESTART WITH 1; +ALTER TABLE users ALTER COLUMN id RESTART WITH 1;; + +INSERT INTO users (name, email) VALUES +('Пользователь №1', 'mail1@mail.ru'), +('Пользователь №2', 'mail2@mail.ru'), +('Пользователь №3', 'mail3@mail.ru'), +('Пользователь №4', 'mail4@mail.ru'), +('Пользователь №5', 'mail5@mail.ru'); + +INSERT INTO item_request (description, requestor_id, created) VALUES +('Нужна вещь №1', 1, '2025-09-12 10:00:00'), +('Нужна вещь №2', 2, '2025-09-13 11:00:00'), +('Нужна вещь №3', 3, '2025-09-14 12:00:00'), +('Нужна вещь №4', 4, '2025-09-15 13:00:00'), +('Нужна вещь №5', 5, '2025-09-16 14:00:00'); + +INSERT INTO item (name, description, available, owner_id, request_id) VALUES +('Название вещи №1', 'Описание вещи №1', true, 1, 1), +('Название вещи №2', 'Описание вещи №2', true, 2, 2), +('Название вещи №3', 'Описание вещи №3', false, 3, 3), +('Название вещи №4', 'Описание вещи №4', true, 4, 4), +('Название вещи №5', 'Описание вещи №5', true, 5, 5); + +INSERT INTO booking (start_time, end_time, item_id, booker_id, status) VALUES +('2025-08-10 09:00:00', '2025-09-10 18:00:00', 1, 2, 'APPROVED'), +('2025-08-11 10:00:00', '2025-09-11 19:00:00', 2, 3, 'WAITING'), +('2025-08-12 11:00:00', '2025-09-12 20:00:00', 3, 1, 'REJECTED'), +('2025-08-13 12:00:00', '2025-09-13 21:00:00', 4, 1, 'APPROVED'), +('2025-08-14 13:00:00', '2025-09-14 22:00:00', 5, 2, 'CANCELED'); + +INSERT INTO comments (text, item_id, author_id, created) VALUES +('Комментарий №1', 1, 1, '2025-08-10 09:00:00'), +('Комментарий №2', 2, 2, '2025-08-10 09:00:00'), +('Комментарий №3', 3, 3, '2025-08-10 09:00:00'), +('Комментарий №4', 4, 4, '2025-08-10 09:00:00'), +('Комментарий №5', 5, 5, '2025-08-10 09:00:00'); \ No newline at end of file diff --git a/server/src/test/resources/schema.sql b/server/src/test/resources/schema.sql new file mode 100644 index 0000000..99cd58f --- /dev/null +++ b/server/src/test/resources/schema.sql @@ -0,0 +1,43 @@ +DROP TABLE IF EXISTS booking CASCADE; +DROP TABLE IF EXISTS item CASCADE; +DROP TABLE IF EXISTS item_request CASCADE; +DROP TABLE IF EXISTS users CASCADE; +DROP TABLE IF EXISTS comments CASCADE; +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_request( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + description VARCHAR(255) NOT NULL, + requestor_id BIGINT NOT NULL REFERENCES users (id), + created TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE TABLE IF NOT EXISTS item( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description VARCHAR(255) NOT NULL, + available BOOLEAN NOT NULL, + owner_id BIGINT NOT NULL REFERENCES users (id), + request_id BIGINT REFERENCES item_request (id) +); + +CREATE TABLE IF NOT EXISTS booking( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + start_time TIMESTAMP WITHOUT TIME ZONE NOT NULL, + end_time TIMESTAMP WITHOUT TIME ZONE, + item_id BIGINT NOT NULL REFERENCES item (id), + booker_id BIGINT NOT NULL REFERENCES users (id), + status VARCHAR(255) NOT NULL +); + +CREATE TABLE IF NOT EXISTS comments( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + text VARCHAR(255) NOT NULL, + item_id BIGINT NOT NULL REFERENCES item (id), + author_id BIGINT NOT NULL REFERENCES users (id), + created TIMESTAMP WITHOUT TIME ZONE NOT NULL +); \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/Booking.java b/src/main/java/ru/practicum/shareit/booking/Booking.java deleted file mode 100644 index 2d9c666..0000000 --- a/src/main/java/ru/practicum/shareit/booking/Booking.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.booking; - -/** - * TODO Sprint add-bookings. - */ -public class Booking { -} diff --git a/src/main/java/ru/practicum/shareit/booking/BookingController.java b/src/main/java/ru/practicum/shareit/booking/BookingController.java deleted file mode 100644 index b94493d..0000000 --- a/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ /dev/null @@ -1,12 +0,0 @@ -package ru.practicum.shareit.booking; - -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * TODO Sprint add-bookings. - */ -@RestController -@RequestMapping(path = "/bookings") -public class BookingController { -} diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java b/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java deleted file mode 100644 index 861de9e..0000000 --- a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.booking.dto; - -/** - * TODO Sprint add-bookings. - */ -public class BookingDto { -} diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/src/main/java/ru/practicum/shareit/item/ItemController.java deleted file mode 100644 index bb17668..0000000 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ /dev/null @@ -1,12 +0,0 @@ -package ru.practicum.shareit.item; - -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * TODO Sprint add-controllers. - */ -@RestController -@RequestMapping("/items") -public class ItemController { -} diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java deleted file mode 100644 index 9319d7d..0000000 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.item.dto; - -/** - * TODO Sprint add-controllers. - */ -public class ItemDto { -} diff --git a/src/main/java/ru/practicum/shareit/item/model/Item.java b/src/main/java/ru/practicum/shareit/item/model/Item.java deleted file mode 100644 index 44eb73d..0000000 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.item.model; - -/** - * TODO Sprint add-controllers. - */ -public class Item { -} diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/ItemRequest.java deleted file mode 100644 index 95d6f23..0000000 --- a/src/main/java/ru/practicum/shareit/request/ItemRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.request; - -/** - * TODO Sprint add-item-requests. - */ -public class ItemRequest { -} diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequestController.java b/src/main/java/ru/practicum/shareit/request/ItemRequestController.java deleted file mode 100644 index 064e2e9..0000000 --- a/src/main/java/ru/practicum/shareit/request/ItemRequestController.java +++ /dev/null @@ -1,12 +0,0 @@ -package ru.practicum.shareit.request; - -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * TODO Sprint add-item-requests. - */ -@RestController -@RequestMapping(path = "/requests") -public class ItemRequestController { -} diff --git a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java b/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java deleted file mode 100644 index 7b3ed54..0000000 --- a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.request.dto; - -/** - * TODO Sprint add-item-requests. - */ -public class ItemRequestDto { -} diff --git a/src/main/java/ru/practicum/shareit/user/User.java b/src/main/java/ru/practicum/shareit/user/User.java deleted file mode 100644 index ae6e7f3..0000000 --- a/src/main/java/ru/practicum/shareit/user/User.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.user; - -/** - * TODO Sprint add-controllers. - */ -public class User { -} diff --git a/src/main/java/ru/practicum/shareit/user/UserController.java b/src/main/java/ru/practicum/shareit/user/UserController.java deleted file mode 100644 index 03039b9..0000000 --- a/src/main/java/ru/practicum/shareit/user/UserController.java +++ /dev/null @@ -1,12 +0,0 @@ -package ru.practicum.shareit.user; - -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * TODO Sprint add-controllers. - */ -@RestController -@RequestMapping(path = "/users") -public class UserController { -} diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties deleted file mode 100644 index 9e9bc4b..0000000 --- a/src/main/resources/application-test.properties +++ /dev/null @@ -1,13 +0,0 @@ -spring.jpa.hibernate.ddl-auto=none -spring.jpa.properties.hibernate.format_sql=true -spring.sql.init.mode=always -logging.level.org.springframework.orm.jpa=INFO -logging.level.org.springframework.transaction=INFO -logging.level.org.springframework.transaction.interceptor=TRACE -logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG - -# TODO Append connection to H2 DB -#spring.datasource.driverClassName -#spring.datasource.url -#spring.datasource.username -#spring.datasource.password diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 51c5180..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,14 +0,0 @@ -spring.jpa.hibernate.ddl-auto=none -spring.jpa.properties.hibernate.format_sql=true -spring.sql.init.mode=always - -logging.level.org.springframework.orm.jpa=INFO -logging.level.org.springframework.transaction=INFO -logging.level.org.springframework.transaction.interceptor=TRACE -logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG - -# TODO Append connection to Postgres DB -#spring.datasource.driverClassName -#spring.datasource.url -#spring.datasource.username -#spring.datasource.password