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