diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..abe6570 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,39 @@ +services: + gateway: + build: gateway + image: shareit-gateway + container_name: shareit-gateway + ports: + - "8080:8080" + depends_on: + - server + environment: + - SHAREIT_SERVER_URL=http://server:9090 + + server: + build: server + image: shareit-server + container_name: shareit-server + ports: + - "9090:9090" + depends_on: + - db + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/shareit + - SPRING_DATASOURCE_USERNAME=shareit + - SPRING_DATASOURCE_PASSWORD=shareit + + db: + image: postgres:16.1 + container_name: postgres + ports: + - "6541:5432" + environment: + - POSTGRES_PASSWORD=shareit + - POSTGRES_USER=shareit + - POSTGRES_DB=shareit + healthcheck: + test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER + timeout: 5s + interval: 5s + retries: 10 \ No newline at end of file diff --git a/gateway/Dockerfile b/gateway/Dockerfile new file mode 100644 index 0000000..308e738 --- /dev/null +++ b/gateway/Dockerfile @@ -0,0 +1,5 @@ +FROM amazoncorretto:21 +VOLUME /tmp +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file diff --git a/gateway/pom.xml b/gateway/pom.xml new file mode 100644 index 0000000..dacdef2 --- /dev/null +++ b/gateway/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + ru.practicum + shareit + 0.0.1-SNAPSHOT + + + gateway + 0.0.1-SNAPSHOT + + ShareIt Gateway + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.hibernate.validator + hibernate-validator + + + + org.apache.httpcomponents.client5 + httpclient5 + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + + true + true + true + checkstyle.xml + + + + + + check + + compile + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/OnCreate.java b/gateway/src/main/java/ru/practicum/shareit/OnCreate.java new file mode 100644 index 0000000..24698fd --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/OnCreate.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit; + +public interface OnCreate { +} diff --git a/gateway/src/main/java/ru/practicum/shareit/OnUpdate.java b/gateway/src/main/java/ru/practicum/shareit/OnUpdate.java new file mode 100644 index 0000000..4d6a567 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/OnUpdate.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit; + +public interface OnUpdate { +} 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..41081fb --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/ShareItGateway.java @@ -0,0 +1,12 @@ +package ru.practicum.shareit; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ShareItGateway { + + public static void main(String[] args) { + SpringApplication.run(ShareItGateway.class, args); + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/Util.java b/gateway/src/main/java/ru/practicum/shareit/Util.java new file mode 100644 index 0000000..9c1a806 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/Util.java @@ -0,0 +1,13 @@ +package ru.practicum.shareit; + +public class Util { + public static final String USER_ID_HEADER = "X-Sharer-User-Id"; + + public static final String EMPTY_PATH = ""; + + public static final String USERS_PATH = "/users"; + public static final String ITEMS_PATH = "/items"; + public static final String BOOKING_PATH = "/bookings"; + public static final String REQUEST_PATH = "/requests"; + +} diff --git a/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java b/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java new file mode 100644 index 0000000..3a3680d --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java @@ -0,0 +1,141 @@ +package ru.practicum.shareit.client; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.*; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.lang.Nullable; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.DefaultUriBuilderFactory; + +import java.util.List; +import java.util.Map; + +public class BaseClient { + + @Value("${shareit-server.url}") + private String serverUrl; + + private final String prefix; + + protected RestTemplate rest; + + @PostConstruct + public void init() { + RestTemplateBuilder builder = new RestTemplateBuilder(); + + this.rest = builder.uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + prefix)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build(); + } + + public BaseClient(String prefix) { + this.prefix = prefix; + } + + protected ResponseEntity get(String path) { + return get(path, null, null); + } + + protected ResponseEntity get(String path, long userId) { + return get(path, userId, null); + } + + protected ResponseEntity get(String path, Long userId, @Nullable Map parameters) { + return makeAndSendRequest(HttpMethod.GET, path, userId, parameters, null); + } + + protected ResponseEntity post(String path, T body) { + return post(path, null, null, body); + } + + protected ResponseEntity post(String path, long userId, T body) { + return post(path, userId, null, body); + } + + protected ResponseEntity post(String path, Long userId, @Nullable Map parameters, T body) { + return makeAndSendRequest(HttpMethod.POST, path, userId, parameters, body); + } + + protected ResponseEntity put(String path, long userId, T body) { + return put(path, userId, null, body); + } + + protected ResponseEntity put(String path, long userId, @Nullable Map parameters, T body) { + return makeAndSendRequest(HttpMethod.PUT, path, userId, parameters, body); + } + + protected ResponseEntity patch(String path, T body) { + return patch(path, null, null, body); + } + + protected ResponseEntity patch(String path, long userId) { + return patch(path, userId, null, null); + } + + protected ResponseEntity patch(String path, long userId, T body) { + return patch(path, userId, null, body); + } + + protected ResponseEntity patch(String path, long userId, Map parameters) { + return patch(path, userId, parameters, null); + } + + protected ResponseEntity patch(String path, Long userId, @Nullable Map parameters, T body) { + return makeAndSendRequest(HttpMethod.PATCH, path, userId, parameters, body); + } + + protected ResponseEntity delete(String path) { + return delete(path, null, null); + } + + protected ResponseEntity delete(String path, long userId) { + return delete(path, userId, null); + } + + protected ResponseEntity delete(String path, Long userId, @Nullable Map parameters) { + return makeAndSendRequest(HttpMethod.DELETE, path, userId, parameters, null); + } + + private ResponseEntity makeAndSendRequest(HttpMethod method, String path, Long userId, @Nullable Map parameters, @Nullable T body) { + HttpEntity requestEntity = new HttpEntity<>(body, defaultHeaders(userId)); + + ResponseEntity shareitServerResponse; + try { + if (parameters != null) { + shareitServerResponse = rest.exchange(path, method, requestEntity, Object.class, parameters); + } else { + shareitServerResponse = rest.exchange(path, method, requestEntity, Object.class); + } + } catch (HttpStatusCodeException e) { + return ResponseEntity.status(e.getStatusCode()).body(e.getResponseBodyAsByteArray()); + } + return prepareGatewayResponse(shareitServerResponse); + } + + private HttpHeaders defaultHeaders(Long userId) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); + if (userId != null) { + headers.set("X-Sharer-User-Id", String.valueOf(userId)); + } + return headers; + } + + private static ResponseEntity prepareGatewayResponse(ResponseEntity response) { + if (response.getStatusCode().is2xxSuccessful()) { + return response; + } + + ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.status(response.getStatusCode()); + + if (response.hasBody()) { + return responseBuilder.body(response.getBody()); + } + + return responseBuilder.build(); + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/client/BookingClient.java b/gateway/src/main/java/ru/practicum/shareit/client/BookingClient.java new file mode 100644 index 0000000..57ff80b --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/client/BookingClient.java @@ -0,0 +1,44 @@ +package ru.practicum.shareit.client; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import ru.practicum.shareit.dto.BookingDto; +import ru.practicum.shareit.dto.BookingState; + +import java.util.Collections; + +import static ru.practicum.shareit.Util.BOOKING_PATH; +import static ru.practicum.shareit.Util.EMPTY_PATH; + +@Component +public class BookingClient extends BaseClient { + public BookingClient() { + super(BOOKING_PATH); + } + + public ResponseEntity createBooking(Long userId, BookingDto dto) { + if (!dto.getStart().isBefore(dto.getEnd())) { + return ResponseEntity.badRequest().build(); + } + + return post(EMPTY_PATH, userId, dto); + } + + public ResponseEntity updateBookingStatus(Long userId, Long bookingId, Boolean approved) { + return patch("/" + bookingId + "?approved={approved}", + userId, + Collections.singletonMap("approved", approved)); + } + + public ResponseEntity getBooking(Long userId, Long bookingId) { + return get("/" + bookingId, userId); + } + + public ResponseEntity findByBookerAndState(Long bookerId, BookingState state) { + return get("?state={state}", bookerId, Collections.singletonMap("state", state)); + } + + public ResponseEntity findByOwnerAndState(Long ownerId, BookingState state) { + return get("/owner?state={state}", ownerId, Collections.singletonMap("state", state)); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/client/ItemClient.java b/gateway/src/main/java/ru/practicum/shareit/client/ItemClient.java new file mode 100644 index 0000000..f438f00 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/client/ItemClient.java @@ -0,0 +1,47 @@ +package ru.practicum.shareit.client; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import ru.practicum.shareit.dto.CommentCreate; +import ru.practicum.shareit.dto.ItemDto; + +import java.util.Collections; + +import static ru.practicum.shareit.Util.EMPTY_PATH; +import static ru.practicum.shareit.Util.ITEMS_PATH; + +@Component +public class ItemClient extends BaseClient { + + public ItemClient() { + super(ITEMS_PATH); + } + + public ResponseEntity getItem(Long itemId) { + return get("/" + itemId); + } + + public ResponseEntity searchItems(String text) { + if (text == null || text.isBlank()) { + return ResponseEntity.ok().body(Collections.emptyList()); + } + + return get("/search?text={text}", null, Collections.singletonMap("text", text)); + } + + public ResponseEntity getItems(Long userId) { + return get(EMPTY_PATH, userId); + } + + public ResponseEntity addItem(Long userId, ItemDto dto) { + return post(EMPTY_PATH, userId, dto); + } + + public ResponseEntity updateItem(Long userId, Long itemId, ItemDto dto) { + return patch("/" + itemId, userId, dto); + } + + public ResponseEntity addComment(Long userId, Long itemId, CommentCreate commentText) { + return post("/" + itemId + "/comment", userId, commentText); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/client/ItemRequestClient.java b/gateway/src/main/java/ru/practicum/shareit/client/ItemRequestClient.java new file mode 100644 index 0000000..69ec020 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/client/ItemRequestClient.java @@ -0,0 +1,33 @@ +package ru.practicum.shareit.client; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import ru.practicum.shareit.dto.ItemRequestCreate; + +import static ru.practicum.shareit.Util.EMPTY_PATH; +import static ru.practicum.shareit.Util.REQUEST_PATH; + +@Component +public class ItemRequestClient extends BaseClient { + public ItemRequestClient() { + super(REQUEST_PATH); + } + + public ResponseEntity addItemRequest(ItemRequestCreate create, Long requestorId) { + + return post(EMPTY_PATH, requestorId, create); + } + + public ResponseEntity getRequest(Long requestId) { + return get("/" + requestId); + } + + public ResponseEntity getRequests(Long userId) { + return get(EMPTY_PATH, userId); + } + + public ResponseEntity getAllRequests() { + return get("/all"); + } + +} diff --git a/gateway/src/main/java/ru/practicum/shareit/client/UserClient.java b/gateway/src/main/java/ru/practicum/shareit/client/UserClient.java new file mode 100644 index 0000000..b592f85 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/client/UserClient.java @@ -0,0 +1,32 @@ +package ru.practicum.shareit.client; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import ru.practicum.shareit.dto.UserDto; + +import static ru.practicum.shareit.Util.EMPTY_PATH; +import static ru.practicum.shareit.Util.USERS_PATH; + +@Component +public class UserClient extends BaseClient { + + public UserClient() { + super(USERS_PATH); + } + + public ResponseEntity getUser(Long userId) { + return get("/" + userId); + } + + public ResponseEntity addUser(UserDto user) { + return post(EMPTY_PATH, user); + } + + public ResponseEntity updateUser(Long userId, UserDto user) { + return patch("/" + userId, user); + } + + public ResponseEntity deleteUser(Long userId) { + return delete("/" + userId); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/controller/BookingController.java b/gateway/src/main/java/ru/practicum/shareit/controller/BookingController.java new file mode 100644 index 0000000..ba20c19 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/controller/BookingController.java @@ -0,0 +1,60 @@ +package ru.practicum.shareit.controller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.client.BookingClient; +import ru.practicum.shareit.dto.BookingDto; +import ru.practicum.shareit.dto.BookingState; + +import static ru.practicum.shareit.Util.USER_ID_HEADER; + +@RestController +@RequestMapping(path = "/bookings") +@RequiredArgsConstructor +public class BookingController { + + private final BookingClient client; + + @PostMapping + public ResponseEntity createBooking(@RequestHeader(USER_ID_HEADER) @Positive Long userId, + @Valid @RequestBody BookingDto bookingDto) { + + return client.createBooking(userId, bookingDto); + } + + @PatchMapping("/{bookingId}") + public ResponseEntity updateBookingStatus( + @RequestHeader(USER_ID_HEADER) @Positive Long userId, + @PathVariable @Positive Long bookingId, + @RequestParam Boolean approved) { + + return client.updateBookingStatus(userId, bookingId, approved); + } + + @GetMapping("/{bookingId}") + public ResponseEntity getBooking( + @RequestHeader(USER_ID_HEADER) @Positive Long userId, + @PathVariable @Positive Long bookingId) { + + return client.getBooking(userId, bookingId); + } + + @GetMapping + public ResponseEntity findByBookerAndState( + @RequestHeader(USER_ID_HEADER) @Positive Long bookerId, + @RequestParam(defaultValue = "ALL") BookingState state) { + + return client.findByBookerAndState(bookerId, state); + } + + @GetMapping("/owner") + public ResponseEntity findByOwnerAndState( + @RequestHeader(USER_ID_HEADER) @Positive Long ownerId, + @RequestParam(defaultValue = "ALL") BookingState state) { + + return client.findByOwnerAndState(ownerId, state); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/controller/ItemController.java b/gateway/src/main/java/ru/practicum/shareit/controller/ItemController.java new file mode 100644 index 0000000..fe714c3 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/controller/ItemController.java @@ -0,0 +1,59 @@ +package ru.practicum.shareit.controller; + +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.OnCreate; +import ru.practicum.shareit.OnUpdate; +import ru.practicum.shareit.client.ItemClient; +import ru.practicum.shareit.dto.CommentCreate; +import ru.practicum.shareit.dto.ItemDto; + +import static ru.practicum.shareit.Util.USER_ID_HEADER; + +@RestController +@RequestMapping("/items") +@RequiredArgsConstructor +public class ItemController { + private final ItemClient client; + + @GetMapping("/{itemId}") + public ResponseEntity getItem(@PathVariable @Positive Long itemId) { + return client.getItem(itemId); + } + + @GetMapping("/search") + public ResponseEntity searchItems(@RequestParam String text) { + return client.searchItems(text); + } + + @GetMapping + public ResponseEntity getItems(@RequestHeader(USER_ID_HEADER) @Positive Long userId) { + return client.getItems(userId); + } + + @PostMapping + public ResponseEntity addItem( + @RequestHeader(USER_ID_HEADER) @Positive Long userId, + @RequestBody @Validated(OnCreate.class) ItemDto itemDto) { + return client.addItem(userId, itemDto); + } + + @PatchMapping("/{itemId}") + public ResponseEntity updateItem( + @RequestHeader(USER_ID_HEADER) @Positive Long userId, + @PathVariable @Positive Long itemId, + @RequestBody @Validated(OnUpdate.class) ItemDto itemDto) { + return client.updateItem(userId, itemId, itemDto); + } + + @PostMapping("/{itemId}/comment") + public ResponseEntity addComment(@RequestHeader(USER_ID_HEADER) @Positive Long userId, + @PathVariable @Positive Long itemId, + @RequestBody CommentCreate commentText) { + + return client.addComment(userId, itemId, commentText); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/controller/ItemRequestController.java b/gateway/src/main/java/ru/practicum/shareit/controller/ItemRequestController.java new file mode 100644 index 0000000..2b0e845 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/controller/ItemRequestController.java @@ -0,0 +1,40 @@ +package ru.practicum.shareit.controller; + +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.client.ItemRequestClient; +import ru.practicum.shareit.dto.ItemRequestCreate; + + +import static ru.practicum.shareit.Util.USER_ID_HEADER; + +@RestController +@RequiredArgsConstructor +@RequestMapping(path = "/requests") +public class ItemRequestController { + + private final ItemRequestClient itemRequestClient; + + @PostMapping + public ResponseEntity addItemRequest(@RequestHeader(name = USER_ID_HEADER) Long requestorId, + @RequestBody ItemRequestCreate itemRequestCreate) { + return itemRequestClient.addItemRequest(itemRequestCreate, requestorId); + } + + @GetMapping + public ResponseEntity getRequests(@RequestHeader(name = USER_ID_HEADER) @Positive Long userId) { + return itemRequestClient.getRequests(userId); + } + + @GetMapping("/all") + public ResponseEntity getAllRequests() { + return itemRequestClient.getAllRequests(); + } + + @GetMapping("/{requestId}") + public ResponseEntity getRequest(@PathVariable @Positive Long requestId) { + return itemRequestClient.getRequest(requestId); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/controller/UserController.java b/gateway/src/main/java/ru/practicum/shareit/controller/UserController.java new file mode 100644 index 0000000..b205ffd --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/controller/UserController.java @@ -0,0 +1,41 @@ +package ru.practicum.shareit.controller; + +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.OnCreate; +import ru.practicum.shareit.OnUpdate; +import ru.practicum.shareit.client.UserClient; +import ru.practicum.shareit.dto.UserDto; + +@RestController +@RequestMapping(path = "/users") +@RequiredArgsConstructor +public class UserController { + + private final UserClient client; + + @GetMapping("/{userId}") + public ResponseEntity getUser(@PathVariable @Positive Long userId) { + return client.getUser(userId); + } + + @PostMapping + public ResponseEntity addUser(@RequestBody @Validated(OnCreate.class) UserDto user) { + return client.addUser(user); + } + + @PatchMapping("/{userId}") + public ResponseEntity updateUser( + @PathVariable @Positive Long userId, + @RequestBody @Validated(OnUpdate.class) UserDto user) { + return client.updateUser(userId, user); + } + + @DeleteMapping("/{userId}") + public ResponseEntity deleteUser(@PathVariable @Positive Long userId) { + return client.deleteUser(userId); + } +} diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java b/gateway/src/main/java/ru/practicum/shareit/dto/BookingDto.java similarity index 90% rename from src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java rename to gateway/src/main/java/ru/practicum/shareit/dto/BookingDto.java index 14e48fa..0c07f51 100644 --- a/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java +++ b/gateway/src/main/java/ru/practicum/shareit/dto/BookingDto.java @@ -1,4 +1,4 @@ -package ru.practicum.shareit.booking.dto; +package ru.practicum.shareit.dto; import jakarta.validation.constraints.FutureOrPresent; import jakarta.validation.constraints.NotNull; diff --git a/gateway/src/main/java/ru/practicum/shareit/dto/BookingState.java b/gateway/src/main/java/ru/practicum/shareit/dto/BookingState.java new file mode 100644 index 0000000..9b6e8b1 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/dto/BookingState.java @@ -0,0 +1,5 @@ +package ru.practicum.shareit.dto; + +public enum BookingState { + ALL, CURRENT, PAST, FUTURE, WAITING, REJECTED +} diff --git a/gateway/src/main/java/ru/practicum/shareit/dto/CommentCreate.java b/gateway/src/main/java/ru/practicum/shareit/dto/CommentCreate.java new file mode 100644 index 0000000..042bb9c --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/dto/CommentCreate.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.dto; + +public record CommentCreate(String text) { +} diff --git a/gateway/src/main/java/ru/practicum/shareit/dto/CommentDto.java b/gateway/src/main/java/ru/practicum/shareit/dto/CommentDto.java new file mode 100644 index 0000000..d33c579 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/dto/CommentDto.java @@ -0,0 +1,14 @@ +package ru.practicum.shareit.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class CommentDto { + private Long id; + private Long itemId; + private String authorName; + private String text; + private LocalDateTime created; +} diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java b/gateway/src/main/java/ru/practicum/shareit/dto/ItemDto.java similarity index 85% rename from src/main/java/ru/practicum/shareit/item/dto/ItemDto.java rename to gateway/src/main/java/ru/practicum/shareit/dto/ItemDto.java index f5de282..ebc5793 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ b/gateway/src/main/java/ru/practicum/shareit/dto/ItemDto.java @@ -1,18 +1,18 @@ -package ru.practicum.shareit.item.dto; +package ru.practicum.shareit.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import ru.practicum.shareit.validation.OnCreate; -import ru.practicum.shareit.validation.OnUpdate; +import ru.practicum.shareit.OnCreate; +import ru.practicum.shareit.OnUpdate; import java.time.LocalDateTime; import java.util.Collection; - @Data @AllArgsConstructor @NoArgsConstructor @@ -29,4 +29,6 @@ public class ItemDto { private LocalDateTime lastBooking; private LocalDateTime nextBooking; private Collection comments; + @Positive + private Long requestId; } diff --git a/gateway/src/main/java/ru/practicum/shareit/dto/ItemRequestCreate.java b/gateway/src/main/java/ru/practicum/shareit/dto/ItemRequestCreate.java new file mode 100644 index 0000000..5128adf --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/dto/ItemRequestCreate.java @@ -0,0 +1,6 @@ +package ru.practicum.shareit.dto; + +import jakarta.validation.constraints.NotBlank; + +public record ItemRequestCreate(@NotBlank String description) { +} diff --git a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java b/gateway/src/main/java/ru/practicum/shareit/dto/UserDto.java similarity index 85% rename from src/main/java/ru/practicum/shareit/user/dto/UserDto.java rename to gateway/src/main/java/ru/practicum/shareit/dto/UserDto.java index bf5f27c..82951f9 100644 --- a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java +++ b/gateway/src/main/java/ru/practicum/shareit/dto/UserDto.java @@ -1,12 +1,12 @@ -package ru.practicum.shareit.user.dto; +package ru.practicum.shareit.dto; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Data; -import ru.practicum.shareit.validation.OnCreate; -import ru.practicum.shareit.validation.OnUpdate; +import ru.practicum.shareit.OnCreate; +import ru.practicum.shareit.OnUpdate; @Data public class UserDto { diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties new file mode 100644 index 0000000..7f0cd35 --- /dev/null +++ b/gateway/src/main/resources/application.properties @@ -0,0 +1,3 @@ +server.port=8080 +shareit-server.url=http://localhost:9090 + diff --git a/gateway/src/test/java/model/BookingTest.java b/gateway/src/test/java/model/BookingTest.java new file mode 100644 index 0000000..e9cc3b4 --- /dev/null +++ b/gateway/src/test/java/model/BookingTest.java @@ -0,0 +1,97 @@ +package model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.dto.BookingDto; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BookingTest extends ModelValidationTest { + private BookingDto dto; + private final LocalDateTime start = LocalDateTime.of(2030, 1, 1, 0, 0); + private final LocalDateTime end = LocalDateTime.of(2030, 1, 2, 0, 0); + + @BeforeEach + public void initDto() { + dto = new BookingDto(); + dto.setItemId(1L); + dto.setStart(start); + dto.setEnd(end); + } + + @Test + public void shouldNotFindViolation() { + assertTrue(isModelValid(dto)); + } + + @Nested + class IdTest { + + @Test + public void shouldFindViolationWhenIdIsNull() { + dto.setItemId(null); + assertFalse(isModelValid(dto)); + } + + @Test + public void shouldFindViolationWhenIdIsZero() { + dto.setItemId(0L); + assertFalse(isModelValid(dto)); + } + + @Test + public void shouldFindViolationWhenIdIsNegative() { + dto.setItemId(-1L); + assertFalse(isModelValid(dto)); + } + } + + @Nested + class StartTest { + + @Test + public void shouldFindViolationWhenStartInPast() { + dto.setStart(start.minusYears(100)); + assertFalse(isModelValid(dto)); + } + + @Test + public void shouldFindViolationWhenStartIsNull() { + dto.setStart(null); + assertFalse(isModelValid(dto)); + } + + @Test + public void shouldNotFindViolationWhenFuture() { + dto.setStart(start.plusYears(100)); + assertTrue(isModelValid(dto)); + } + } + + @Nested + class EndTest { + + @Test + public void shouldFindViolationWhenEndInPast() { + dto.setEnd(end.minusYears(100)); + assertFalse(isModelValid(dto)); + } + + @Test + public void shouldFindViolationWhenEndIsNull() { + dto.setEnd(null); + assertFalse(isModelValid(dto)); + } + + @Test + public void shouldNotFindViolationWhenFuture() { + dto.setEnd(end.plusYears(100)); + assertTrue(isModelValid(dto)); + } + } + +} diff --git a/gateway/src/test/java/model/ItemTest.java b/gateway/src/test/java/model/ItemTest.java new file mode 100644 index 0000000..496b0c3 --- /dev/null +++ b/gateway/src/test/java/model/ItemTest.java @@ -0,0 +1,117 @@ +package model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.dto.ItemDto; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ItemTest extends ModelValidationTest { + + private ItemDto dto; + + @BeforeEach + public void initDto() { + dto = new ItemDto(); + dto.setId(1L); + dto.setName("John"); + dto.setDescription("John Description"); + dto.setAvailable(true); + dto.setRequestId(1L); + } + + @Test + public void shouldNotFindViolation() { + assertTrue(isModelValid(dto)); + } + + @Test + public void shouldFindViolationWhenAvailableIsNullForCreate() { + dto.setAvailable(null); + assertFalse(isModelValidForCreate(dto)); + } + + @Test + public void shouldNotFindViolationWhenAvailableIsNullForUpdate() { + dto.setAvailable(null); + assertTrue(isModelValidForUpdate(dto)); + } + + @Nested + class IdTest { + + @Test + public void shouldFindViolationWhenIdIsZero() { + dto.setRequestId(0L); + assertFalse(isModelValid(dto)); + } + + @Test + public void shouldFindViolationWhenIdIsNegative() { + dto.setRequestId(-1L); + assertFalse(isModelValid(dto)); + } + } + + @Nested + class NameTest { + + @Test + public void shouldNotFindViolationWhenNameIsNullOnUpdate() { + dto.setName(null); + assertTrue(isModelValidForUpdate(dto)); + } + + @Test + public void shouldFindViolationWhenNameIsNull() { + dto.setName(null); + assertFalse(isModelValidForCreate(dto)); + } + + @Test + public void shouldFindViolationWhenNameIsEmpty() { + dto.setName(""); + assertFalse(isModelValidForCreate(dto)); + assertFalse(isModelValidForUpdate(dto)); + } + + @Test + public void shouldFindViolationWhenNameIsBlank() { + dto.setName(" "); + assertFalse(isModelValidForCreate(dto)); + assertTrue(isModelValidForUpdate(dto)); + } + } + + @Nested + class DescriptionTest { + + @Test + public void shouldNotFindViolationWhenDescriptionIsNullOnUpdate() { + dto.setDescription(null); + assertTrue(isModelValidForUpdate(dto)); + } + + @Test + public void shouldFindViolationWhenDescriptionIsNull() { + dto.setDescription(null); + assertFalse(isModelValidForCreate(dto)); + } + + @Test + public void shouldFindViolationWhenDescriptionIsEmpty() { + dto.setDescription(""); + assertFalse(isModelValidForCreate(dto)); + assertFalse(isModelValidForUpdate(dto)); + } + + @Test + public void shouldFindViolationWhenDescriptionIsBlank() { + dto.setDescription(" "); + assertFalse(isModelValidForCreate(dto)); + assertTrue(isModelValidForUpdate(dto)); + } + } +} diff --git a/gateway/src/test/java/model/ModelValidationTest.java b/gateway/src/test/java/model/ModelValidationTest.java new file mode 100644 index 0000000..b42769d --- /dev/null +++ b/gateway/src/test/java/model/ModelValidationTest.java @@ -0,0 +1,28 @@ +package model; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import org.junit.jupiter.api.BeforeAll; +import ru.practicum.shareit.OnCreate; +import ru.practicum.shareit.OnUpdate; + +public class ModelValidationTest { + protected static Validator validator; + + @BeforeAll + public static void setValidator() { + validator = Validation.buildDefaultValidatorFactory().getValidator(); + } + + protected boolean isModelValid(T model) { + return validator.validate(model).isEmpty(); + } + + protected boolean isModelValidForCreate(T model) { + return validator.validate(model, OnCreate.class).isEmpty(); + } + + protected boolean isModelValidForUpdate(T model) { + return validator.validate(model, OnUpdate.class).isEmpty(); + } +} diff --git a/gateway/src/test/java/model/UserTest.java b/gateway/src/test/java/model/UserTest.java new file mode 100644 index 0000000..083bbf6 --- /dev/null +++ b/gateway/src/test/java/model/UserTest.java @@ -0,0 +1,74 @@ +package model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.dto.UserDto; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class UserTest extends ModelValidationTest { + + private UserDto dto; + + @BeforeEach + public void initDto() { + dto = new UserDto(); + dto.setName("John"); + dto.setEmail("john@example.com"); + } + + @Nested + class NameTest { + + @Test + public void shouldNotFindViolationWhenNameIsNullOnUpdate() { + dto.setName(null); + assertTrue(isModelValidForUpdate(dto)); + } + + @Test + public void shouldFindViolationWhenNameIsNull() { + dto.setName(null); + assertFalse(isModelValidForCreate(dto)); + } + + @Test + public void shouldFindViolationWhenNameIsEmpty() { + dto.setName(""); + assertFalse(isModelValidForCreate(dto)); + assertFalse(isModelValidForUpdate(dto)); + } + + @Test + public void shouldFindViolationWhenNameIsBlank() { + dto.setName(" "); + assertFalse(isModelValidForCreate(dto)); + assertTrue(isModelValidForUpdate(dto)); + } + } + + @Nested + class EmailTest { + + @Test + public void shouldFinaViolationWhenEmailIsInvalidFormat() { + dto.setEmail(" asdas d @@ asd"); + assertFalse(isModelValidForCreate(dto)); + assertFalse(isModelValidForUpdate(dto)); + } + + @Test + public void shouldFinaViolationWhenEmailNullOnCreate() { + dto.setEmail(null); + assertFalse(isModelValidForCreate(dto)); + } + + @Test + public void shouldNotFinaViolationWhenEmailNullOnUpdate() { + dto.setEmail(null); + assertTrue(isModelValidForUpdate(dto)); + } + } +} diff --git a/pom.xml b/pom.xml index 2b0e3ee..65bb475 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,7 @@ ru.practicum shareit + pom 0.0.1-SNAPSHOT ShareIt @@ -20,98 +21,29 @@ 3.3.1 - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - org.postgresql - postgresql - runtime - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - - org.projectlombok - lombok - true - - - - com.h2database - h2 - test - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-starter-validation - - + + gateway + server + - - - src/main/resources - true - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - ${maven-checkstyle-plugin.version} - - - true - true - true - checkstyle.xml - - - - - - check - - compile - - - - + + org.springframework.boot + spring-boot-maven-plugin + + + true + + + + org.projectlombok + lombok + + + + org.apache.maven.plugins maven-surefire-plugin @@ -258,17 +190,5 @@ - - coverage - - - - org.jacoco - jacoco-maven-plugin - - - - - - + \ No newline at end of file diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..a840085 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,5 @@ +FROM amazoncorretto:21 +VOLUME /tmp +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] diff --git a/server/pom.xml b/server/pom.xml new file mode 100644 index 0000000..6657d9e --- /dev/null +++ b/server/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + + ru.practicum + shareit + 0.0.1-SNAPSHOT + + + shareit-server + 0.0.1-SNAPSHOT + + ShareIt Server + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.postgresql + postgresql + runtime + + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + + true + true + true + checkstyle.xml + + + + + + check + + compile + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + coverage + + + + org.jacoco + jacoco-maven-plugin + + + + + + + \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/Constants.java b/server/src/main/java/ru/practicum/shareit/Constants.java similarity index 83% rename from src/main/java/ru/practicum/shareit/Constants.java rename to server/src/main/java/ru/practicum/shareit/Constants.java index efa7be6..808f7c3 100644 --- a/src/main/java/ru/practicum/shareit/Constants.java +++ b/server/src/main/java/ru/practicum/shareit/Constants.java @@ -1,6 +1,10 @@ package ru.practicum.shareit; public class Constants { + + private Constants() { + } + public static final String USER_ID_HEADER = "X-Sharer-User-Id"; public static final String ALL = "ALL"; } diff --git a/src/main/java/ru/practicum/shareit/ShareItApp.java b/server/src/main/java/ru/practicum/shareit/ShareItApp.java similarity index 100% rename from src/main/java/ru/practicum/shareit/ShareItApp.java rename to server/src/main/java/ru/practicum/shareit/ShareItApp.java 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 72% rename from src/main/java/ru/practicum/shareit/booking/BookingController.java rename to server/src/main/java/ru/practicum/shareit/booking/BookingController.java index 501fc78..f62abc4 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/server/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -1,7 +1,5 @@ package ru.practicum.shareit.booking; -import jakarta.validation.Valid; -import jakarta.validation.constraints.Positive; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import ru.practicum.shareit.booking.dto.BookingDto; @@ -14,9 +12,6 @@ import static ru.practicum.shareit.Constants.ALL; import static ru.practicum.shareit.Constants.USER_ID_HEADER; -/** - * TODO Sprint add-bookings. - */ @RestController @RequestMapping(path = "/bookings") @RequiredArgsConstructor @@ -25,16 +20,16 @@ public class BookingController { private final BookingService bookingService; @PostMapping - public Booking createBooking(@RequestHeader(USER_ID_HEADER) @Positive Long userId, - @Valid @RequestBody BookingDto bookingDto) { + public Booking createBooking(@RequestHeader(USER_ID_HEADER) Long userId, + @RequestBody BookingDto bookingDto) { return bookingService.createBooking(userId, bookingDto); } @PatchMapping("/{bookingId}") public Booking updateBookingStatus( - @RequestHeader(USER_ID_HEADER) @Positive Long userId, - @PathVariable @Positive Long bookingId, + @RequestHeader(USER_ID_HEADER) Long userId, + @PathVariable Long bookingId, @RequestParam Boolean approved) { return bookingService.updateBookingStatus(userId, bookingId, approved); @@ -42,15 +37,15 @@ public Booking updateBookingStatus( @GetMapping("/{bookingId}") public Booking getBooking( - @RequestHeader(USER_ID_HEADER) @Positive Long userId, - @PathVariable @Positive Long bookingId) { + @RequestHeader(USER_ID_HEADER) Long userId, + @PathVariable Long bookingId) { return bookingService.getBooking(userId, bookingId); } @GetMapping public Collection findByBookerAndState( - @RequestHeader(USER_ID_HEADER) @Positive Long bookerId, + @RequestHeader(USER_ID_HEADER) Long bookerId, @RequestParam(defaultValue = ALL) BookingState state) { return bookingService.findByBookerAndState(bookerId, state); @@ -58,7 +53,7 @@ public Collection findByBookerAndState( @GetMapping("/owner") public Collection findByOwnerAndState( - @RequestHeader(USER_ID_HEADER) @Positive Long ownerId, + @RequestHeader(USER_ID_HEADER) Long ownerId, @RequestParam(defaultValue = ALL) BookingState state) { return bookingService.findByOwnerAndState(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/dao/BookingRepository.java b/server/src/main/java/ru/practicum/shareit/booking/dao/BookingRepository.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/dao/BookingRepository.java rename to server/src/main/java/ru/practicum/shareit/booking/dao/BookingRepository.java 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..2ef27bd --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingDto.java @@ -0,0 +1,12 @@ +package ru.practicum.shareit.booking.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class BookingDto { + private Long itemId; + private LocalDateTime start; + private LocalDateTime end; +} diff --git a/src/main/java/ru/practicum/shareit/booking/dto/LastAndNextDate.java b/server/src/main/java/ru/practicum/shareit/booking/dto/LastAndNextDate.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/dto/LastAndNextDate.java rename to server/src/main/java/ru/practicum/shareit/booking/dto/LastAndNextDate.java diff --git a/src/main/java/ru/practicum/shareit/booking/enums/BookingState.java b/server/src/main/java/ru/practicum/shareit/booking/enums/BookingState.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/enums/BookingState.java rename to server/src/main/java/ru/practicum/shareit/booking/enums/BookingState.java diff --git a/src/main/java/ru/practicum/shareit/booking/enums/BookingStatus.java b/server/src/main/java/ru/practicum/shareit/booking/enums/BookingStatus.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/enums/BookingStatus.java rename to server/src/main/java/ru/practicum/shareit/booking/enums/BookingStatus.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 96% 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 index 7668627..9adc030 100644 --- a/src/main/java/ru/practicum/shareit/booking/model/Booking.java +++ b/server/src/main/java/ru/practicum/shareit/booking/model/Booking.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import ru.practicum.shareit.booking.enums.BookingStatus; @@ -11,6 +12,7 @@ import java.time.LocalDateTime; @Data +@Builder @AllArgsConstructor @NoArgsConstructor @Entity diff --git a/src/main/java/ru/practicum/shareit/booking/service/BookingService.java b/server/src/main/java/ru/practicum/shareit/booking/service/BookingService.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/service/BookingService.java rename to server/src/main/java/ru/practicum/shareit/booking/service/BookingService.java diff --git a/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java b/server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java rename to server/src/main/java/ru/practicum/shareit/booking/service/BookingServiceImpl.java diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/server/src/main/java/ru/practicum/shareit/item/ItemController.java similarity index 66% rename from src/main/java/ru/practicum/shareit/item/ItemController.java rename to server/src/main/java/ru/practicum/shareit/item/ItemController.java index ba963fe..c106ad5 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/server/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -1,15 +1,11 @@ package ru.practicum.shareit.item; -import jakarta.validation.constraints.Positive; import lombok.RequiredArgsConstructor; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.shareit.item.dto.CommentCreate; import ru.practicum.shareit.item.dto.CommentDto; import ru.practicum.shareit.item.dto.ItemDto; import ru.practicum.shareit.item.service.ItemService; -import ru.practicum.shareit.validation.OnCreate; -import ru.practicum.shareit.validation.OnUpdate; import java.util.Collection; @@ -23,7 +19,7 @@ public class ItemController { private final ItemService itemService; @GetMapping("/{itemId}") - public ItemDto getItem(@PathVariable @Positive Long itemId) { + public ItemDto getItem(@PathVariable Long itemId) { return itemService.getItem(itemId); } @@ -33,28 +29,29 @@ public Collection searchItems(@RequestParam String text) { } @GetMapping - public Collection getItems(@RequestHeader(USER_ID_HEADER) @Positive Long userId) { + public Collection getItems(@RequestHeader(USER_ID_HEADER) Long userId) { + System.out.println("hello"); return itemService.getItems(userId); } @PostMapping public ItemDto addItem( - @RequestHeader(USER_ID_HEADER) @Positive Long userId, - @RequestBody @Validated(OnCreate.class) ItemDto itemDto) { + @RequestHeader(USER_ID_HEADER) Long userId, + @RequestBody ItemDto itemDto) { return itemService.addItem(userId, itemDto); } @PatchMapping("/{itemId}") public ItemDto updateItem( - @RequestHeader(USER_ID_HEADER) @Positive Long userId, - @PathVariable @Positive Long itemId, - @RequestBody @Validated(OnUpdate.class) ItemDto itemDto) { + @RequestHeader(USER_ID_HEADER) Long userId, + @PathVariable Long itemId, + @RequestBody ItemDto itemDto) { return itemService.updateItem(userId, itemId, itemDto); } @PostMapping("/{itemId}/comment") - public CommentDto addComment(@RequestHeader(USER_ID_HEADER) @Positive Long userId, - @PathVariable @Positive Long itemId, + public CommentDto addComment(@RequestHeader(USER_ID_HEADER) Long userId, + @PathVariable Long itemId, @RequestBody CommentCreate commentText) { return itemService.addComment(userId, itemId, commentText); diff --git a/src/main/java/ru/practicum/shareit/item/dao/CommentRepository.java b/server/src/main/java/ru/practicum/shareit/item/dao/CommentRepository.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/dao/CommentRepository.java rename to server/src/main/java/ru/practicum/shareit/item/dao/CommentRepository.java diff --git a/src/main/java/ru/practicum/shareit/item/dao/ItemRepository.java b/server/src/main/java/ru/practicum/shareit/item/dao/ItemRepository.java similarity index 60% rename from src/main/java/ru/practicum/shareit/item/dao/ItemRepository.java rename to server/src/main/java/ru/practicum/shareit/item/dao/ItemRepository.java index 5592d22..6fa9cb5 100644 --- a/src/main/java/ru/practicum/shareit/item/dao/ItemRepository.java +++ b/server/src/main/java/ru/practicum/shareit/item/dao/ItemRepository.java @@ -2,6 +2,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import ru.practicum.shareit.item.dto.ItemShortWithRequestId; import ru.practicum.shareit.item.model.Item; import java.util.Collection; @@ -10,12 +11,16 @@ public interface ItemRepository extends JpaRepository { Collection findByOwnerId(Long userId); + Collection findByRequestId(Long requestId); + + Collection findAllByRequestIdIn(Collection ids); + @Query(""" SELECT i FROM Item i WHERE i.isAvailable = true - AND (LOWER(i.name) LIKE LOWER(%:text%) - OR LOWER(i.description) LIKE LOWER(%:text%)) + AND UPPER(i.name) LIKE CONCAT('%',UPPER(:text),'%') OR + UPPER(i.description) LIKE CONCAT('%',UPPER(:text),'%') """) Collection searchByNameOrDescription(String text); } diff --git a/src/main/java/ru/practicum/shareit/item/dto/CommentCreate.java b/server/src/main/java/ru/practicum/shareit/item/dto/CommentCreate.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/dto/CommentCreate.java rename to server/src/main/java/ru/practicum/shareit/item/dto/CommentCreate.java diff --git a/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/dto/CommentDto.java rename to server/src/main/java/ru/practicum/shareit/item/dto/CommentDto.java diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java new file mode 100644 index 0000000..073a67a --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -0,0 +1,22 @@ +package ru.practicum.shareit.item.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Collection; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ItemDto { + private Long id; + private String name; + private String description; + private Boolean available; + private LocalDateTime lastBooking; + private LocalDateTime nextBooking; + private Collection comments; + private Long requestId; +} diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/ItemShort.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemShort.java new file mode 100644 index 0000000..2acb554 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/dto/ItemShort.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.item.dto; + +public record ItemShort(Long id, String name, Long ownerId) { +} diff --git a/server/src/main/java/ru/practicum/shareit/item/dto/ItemShortWithRequestId.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemShortWithRequestId.java new file mode 100644 index 0000000..0807909 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/dto/ItemShortWithRequestId.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.item.dto; + +public interface ItemShortWithRequestId { + Long getId(); + + String getName(); + + Long getOwnerId(); + + Long getRequestId(); +} diff --git a/src/main/java/ru/practicum/shareit/item/mappers/CommentMapper.java b/server/src/main/java/ru/practicum/shareit/item/mappers/CommentMapper.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/mappers/CommentMapper.java rename to server/src/main/java/ru/practicum/shareit/item/mappers/CommentMapper.java diff --git a/src/main/java/ru/practicum/shareit/item/mappers/ItemMapper.java b/server/src/main/java/ru/practicum/shareit/item/mappers/ItemMapper.java similarity index 73% rename from src/main/java/ru/practicum/shareit/item/mappers/ItemMapper.java rename to server/src/main/java/ru/practicum/shareit/item/mappers/ItemMapper.java index e886ca2..19a500b 100644 --- a/src/main/java/ru/practicum/shareit/item/mappers/ItemMapper.java +++ b/server/src/main/java/ru/practicum/shareit/item/mappers/ItemMapper.java @@ -1,6 +1,8 @@ package ru.practicum.shareit.item.mappers; import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.ItemShort; +import ru.practicum.shareit.item.dto.ItemShortWithRequestId; import ru.practicum.shareit.item.model.Item; public class ItemMapper { @@ -10,6 +12,7 @@ public static Item toItem(ItemDto itemDto) { item.setName(itemDto.getName()); item.setDescription(itemDto.getDescription()); item.setIsAvailable(itemDto.getAvailable()); + item.setRequestId(itemDto.getRequestId()); return item; } @@ -32,7 +35,14 @@ public static Item merge(Item item, ItemDto itemDto) { if (itemDto.getAvailable() != null) { item.setIsAvailable(itemDto.getAvailable()); } + if (itemDto.getRequestId() != null) { + item.setRequestId(itemDto.getRequestId()); + } return item; } + + public static ItemShort toItemShort(ItemShortWithRequestId s) { + return new ItemShort(s.getId(), s.getName(), s.getOwnerId()); + } } 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 87% 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 5b85018..9cbd067 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/server/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -2,10 +2,12 @@ import jakarta.persistence.*; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data +@Builder @AllArgsConstructor @NoArgsConstructor @Entity @@ -22,4 +24,6 @@ public class Item { private Boolean isAvailable; @Column(name = "owner_id", nullable = false) private Long ownerId; + @Column(name = "request_id") + private Long requestId; } diff --git a/src/main/java/ru/practicum/shareit/item/service/ItemService.java b/server/src/main/java/ru/practicum/shareit/item/service/ItemService.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/service/ItemService.java rename to server/src/main/java/ru/practicum/shareit/item/service/ItemService.java diff --git a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java b/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java similarity index 99% rename from src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java rename to server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java index d8cd5eb..4c691ac 100644 --- a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java +++ b/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java @@ -83,6 +83,8 @@ public ItemDto addItem(Long userId, ItemDto itemDto) { Item item = ItemMapper.toItem(itemDto); item.setOwnerId(userId); + System.out.println(item); + return ItemMapper.toItemDto(itemRepository.save(item)); } 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..79a8789 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/ItemRequestController.java @@ -0,0 +1,40 @@ +package ru.practicum.shareit.request; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.request.dto.ItemRequestCreate; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.service.ItemRequestService; + +import java.util.Collection; + +import static ru.practicum.shareit.Constants.USER_ID_HEADER; + +@RestController +@RequiredArgsConstructor +@RequestMapping(path = "/requests") +public class ItemRequestController { + + private final ItemRequestService itemRequestService; + + @PostMapping + public ItemRequestDto addItemRequest(@RequestHeader(name = USER_ID_HEADER) Long requestorId, + @RequestBody ItemRequestCreate itemRequestCreate) { + return itemRequestService.addItemRequest(itemRequestCreate, requestorId); + } + + @GetMapping + public Collection getRequests(@RequestHeader(name = USER_ID_HEADER) Long userId) { + return itemRequestService.getRequests(userId); + } + + @GetMapping("/all") + public Collection getAllRequests() { + return itemRequestService.getAllRequests(); + } + + @GetMapping("/{requestId}") + public ItemRequestDto getRequest(@PathVariable Long requestId) { + return itemRequestService.getRequest(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..8112947 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/ItemRequestMapper.java @@ -0,0 +1,27 @@ +package ru.practicum.shareit.request; + +import ru.practicum.shareit.request.dto.ItemRequestCreate; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.model.ItemRequest; + +import java.time.LocalDateTime; + +public class ItemRequestMapper { + public static ItemRequest toItemRequest(ItemRequestCreate create, Long requestorId) { + ItemRequest itemRequest = new ItemRequest(); + itemRequest.setDescription(create.description()); + itemRequest.setRequestorId(requestorId); + itemRequest.setCreated(LocalDateTime.now()); + + return itemRequest; + } + + public static ItemRequestDto toItemRequestDto(ItemRequest itemRequest) { + ItemRequestDto dto = new ItemRequestDto(); + dto.setId(itemRequest.getId()); + dto.setDescription(itemRequest.getDescription()); + dto.setCreated(itemRequest.getCreated()); + + return dto; + } +} diff --git a/server/src/main/java/ru/practicum/shareit/request/dao/ItemRequestRepository.java b/server/src/main/java/ru/practicum/shareit/request/dao/ItemRequestRepository.java new file mode 100644 index 0000000..9ae3960 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/dao/ItemRequestRepository.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.request.dao; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.shareit.request.model.ItemRequest; + +import java.util.Collection; + +public interface ItemRequestRepository extends JpaRepository { + + Collection findByRequestorIdOrderByCreatedDesc(Long requestorId); +} diff --git a/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestCreate.java b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestCreate.java new file mode 100644 index 0000000..9bc5e38 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestCreate.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.request.dto; + +public record ItemRequestCreate(String description) { +} 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..f5f41c2 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java @@ -0,0 +1,15 @@ +package ru.practicum.shareit.request.dto; + +import lombok.Data; +import ru.practicum.shareit.item.dto.ItemShort; + +import java.time.LocalDateTime; +import java.util.Collection; + +@Data +public class ItemRequestDto { + private Long id; + private String description; + private LocalDateTime created; + private Collection items; +} 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..234369d --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java @@ -0,0 +1,27 @@ +package ru.practicum.shareit.request.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "item_requests", schema = "public") +public class ItemRequest { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(name = "description") + private String description; + @Column(name = "requestor_id") + private Long requestorId; + @Column(name = "created_at") + private LocalDateTime created; +} 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..290c327 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java @@ -0,0 +1,106 @@ +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.item.dao.ItemRepository; +import ru.practicum.shareit.item.dto.ItemShort; +import ru.practicum.shareit.item.dto.ItemShortWithRequestId; +import ru.practicum.shareit.item.mappers.ItemMapper; +import ru.practicum.shareit.request.ItemRequestMapper; +import ru.practicum.shareit.request.dao.ItemRequestRepository; +import ru.practicum.shareit.request.dto.ItemRequestCreate; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.validation.exceptions.NotFoundException; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ItemRequestService { + + private final ItemRequestRepository requestRepository; + private final ItemRepository itemRepository; + + public ItemRequestDto addItemRequest(ItemRequestCreate create, Long requestorId) { + ItemRequest ir = requestRepository.save(ItemRequestMapper.toItemRequest(create, requestorId)); + + return ItemRequestMapper.toItemRequestDto(ir); + } + + @Transactional(readOnly = true) + public Collection getRequests(Long userId) { + Collection irs = requestRepository.findByRequestorIdOrderByCreatedDesc(userId); + + return enrichWithItems(irs); + } + + @Transactional(readOnly = true) + public Collection getAllRequests() { + Collection irs = requestRepository.findAll(Sort.by("created").descending()); + + return enrichWithItems(irs); + } + + @Transactional + public ItemRequestDto getRequest(Long requestId) { + + ItemRequest ir = requestRepository.findById(requestId) + .orElseThrow(() -> new NotFoundException("Запрос с id " + requestId + " не найден")); + + Collection items = itemRepository.findByRequestId(requestId) + .stream() + .map(ItemMapper::toItemShort) + .toList(); + + ItemRequestDto dto = ItemRequestMapper.toItemRequestDto(ir); + dto.setItems(items); + + return dto; + } + + /** + * Обогащает коллекцию ItemRequest представлениями предметов. + * + * @param irs коллекция с запросами для обогащения. + * @return коллекцию ItemRequestDto. + */ + private Collection enrichWithItems(Collection irs) { + Collection ids = irs.stream() + .map(ItemRequest::getId) + .toList(); + + Map> map = getNamesMap(ids); + + return irs.stream().map(itemRequest -> { + ItemRequestDto dto = ItemRequestMapper.toItemRequestDto(itemRequest); + dto.setItems(map.get(itemRequest.getId())); + + return dto; + }).toList(); + } + + /** + * Создает таблицу с представлениями предмета. + * + * @param ids список id запросов + * @return {@code Map} где ключ - Id запроса + */ + private Map> getNamesMap(Collection ids) { + Collection nameAndUserIds = itemRepository.findAllByRequestIdIn(ids); + + return nameAndUserIds.stream() + .collect(Collectors.groupingBy( + ItemShortWithRequestId::getRequestId, + Collectors.mapping( + ItemMapper::toItemShort, + Collectors.toList() + ) + )); + } +} 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 57% rename from src/main/java/ru/practicum/shareit/user/UserController.java rename to server/src/main/java/ru/practicum/shareit/user/UserController.java index 4da9340..9567944 100644 --- a/src/main/java/ru/practicum/shareit/user/UserController.java +++ b/server/src/main/java/ru/practicum/shareit/user/UserController.java @@ -1,18 +1,11 @@ package ru.practicum.shareit.user; -import jakarta.validation.constraints.Positive; import lombok.RequiredArgsConstructor; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.shareit.user.dto.UserDto; import ru.practicum.shareit.user.model.User; import ru.practicum.shareit.user.service.UserService; -import ru.practicum.shareit.validation.OnCreate; -import ru.practicum.shareit.validation.OnUpdate; -/** - * TODO Sprint add-controllers. - */ @RestController @RequestMapping(path = "/users") @RequiredArgsConstructor @@ -21,24 +14,24 @@ public class UserController { private final UserService userService; @GetMapping("/{userId}") - public User getUser(@PathVariable @Positive Long userId) { + public User getUser(@PathVariable Long userId) { return userService.getUser(userId); } @PostMapping - public User addUser(@RequestBody @Validated(OnCreate.class) UserDto user) { + public User addUser(@RequestBody UserDto user) { return userService.addUser(user); } @PatchMapping("/{userId}") public User updateUser( - @PathVariable @Positive Long userId, - @RequestBody @Validated(OnUpdate.class) UserDto user) { + @PathVariable Long userId, + @RequestBody UserDto user) { return userService.updateUser(userId, user); } @DeleteMapping("/{userId}") - public void deleteUser(@PathVariable @Positive Long userId) { + public void deleteUser(@PathVariable Long userId) { userService.deleteUser(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/dao/UserRepository.java b/server/src/main/java/ru/practicum/shareit/user/dao/UserRepository.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/dao/UserRepository.java rename to server/src/main/java/ru/practicum/shareit/user/dao/UserRepository.java diff --git a/server/src/main/java/ru/practicum/shareit/user/dto/UserDto.java b/server/src/main/java/ru/practicum/shareit/user/dto/UserDto.java new file mode 100644 index 0000000..1cd40c2 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/dto/UserDto.java @@ -0,0 +1,9 @@ +package ru.practicum.shareit.user.dto; + +import lombok.Data; + +@Data +public class UserDto { + private String name; + private String email; +} 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 93% 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 index 122e90c..82ccf2a 100644 --- a/src/main/java/ru/practicum/shareit/user/model/User.java +++ b/server/src/main/java/ru/practicum/shareit/user/model/User.java @@ -2,10 +2,12 @@ import jakarta.persistence.*; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data +@Builder @AllArgsConstructor @NoArgsConstructor @Entity diff --git a/src/main/java/ru/practicum/shareit/user/service/UserService.java b/server/src/main/java/ru/practicum/shareit/user/service/UserService.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/service/UserService.java rename to server/src/main/java/ru/practicum/shareit/user/service/UserService.java diff --git a/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java b/server/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java rename to server/src/main/java/ru/practicum/shareit/user/service/UserServiceImpl.java diff --git a/src/main/java/ru/practicum/shareit/validation/GlobalExceptionHandler.java b/server/src/main/java/ru/practicum/shareit/validation/GlobalExceptionHandler.java similarity index 88% rename from src/main/java/ru/practicum/shareit/validation/GlobalExceptionHandler.java rename to server/src/main/java/ru/practicum/shareit/validation/GlobalExceptionHandler.java index 7d246e7..5306dcc 100644 --- a/src/main/java/ru/practicum/shareit/validation/GlobalExceptionHandler.java +++ b/server/src/main/java/ru/practicum/shareit/validation/GlobalExceptionHandler.java @@ -10,7 +10,6 @@ import ru.practicum.shareit.validation.exceptions.BadRequestException; import ru.practicum.shareit.validation.exceptions.ForbiddenException; import ru.practicum.shareit.validation.exceptions.NotFoundException; -import ru.practicum.shareit.validation.exceptions.NotUniqueException; import java.util.Collections; import java.util.HashMap; @@ -38,11 +37,6 @@ public ResponseEntity> handleNotFoundException(NotFoundExcep return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Collections.singletonMap("error", ex.getMessage())); } - @ExceptionHandler(NotUniqueException.class) - public ResponseEntity> handleNotUniqueException(NotUniqueException ex) { - return ResponseEntity.status(HttpStatus.CONFLICT).body(Collections.singletonMap("error", ex.getMessage())); - } - @ExceptionHandler(BadRequestException.class) public ResponseEntity> handleBadRequestException(BadRequestException ex) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Collections.singletonMap("error", ex.getMessage())); diff --git a/src/main/java/ru/practicum/shareit/validation/OnCreate.java b/server/src/main/java/ru/practicum/shareit/validation/OnCreate.java similarity index 100% rename from src/main/java/ru/practicum/shareit/validation/OnCreate.java rename to server/src/main/java/ru/practicum/shareit/validation/OnCreate.java diff --git a/src/main/java/ru/practicum/shareit/validation/OnUpdate.java b/server/src/main/java/ru/practicum/shareit/validation/OnUpdate.java similarity index 100% rename from src/main/java/ru/practicum/shareit/validation/OnUpdate.java rename to server/src/main/java/ru/practicum/shareit/validation/OnUpdate.java diff --git a/src/main/java/ru/practicum/shareit/validation/exceptions/BadRequestException.java b/server/src/main/java/ru/practicum/shareit/validation/exceptions/BadRequestException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/validation/exceptions/BadRequestException.java rename to server/src/main/java/ru/practicum/shareit/validation/exceptions/BadRequestException.java diff --git a/src/main/java/ru/practicum/shareit/validation/exceptions/ForbiddenException.java b/server/src/main/java/ru/practicum/shareit/validation/exceptions/ForbiddenException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/validation/exceptions/ForbiddenException.java rename to server/src/main/java/ru/practicum/shareit/validation/exceptions/ForbiddenException.java diff --git a/src/main/java/ru/practicum/shareit/validation/exceptions/NotFoundException.java b/server/src/main/java/ru/practicum/shareit/validation/exceptions/NotFoundException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/validation/exceptions/NotFoundException.java rename to server/src/main/java/ru/practicum/shareit/validation/exceptions/NotFoundException.java diff --git a/src/main/resources/application-test.properties b/server/src/main/resources/application-test.properties similarity index 100% rename from src/main/resources/application-test.properties rename to server/src/main/resources/application-test.properties diff --git a/src/main/resources/application.properties b/server/src/main/resources/application.properties similarity index 96% rename from src/main/resources/application.properties rename to server/src/main/resources/application.properties index 0478d27..08a20e8 100644 --- a/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -1,3 +1,5 @@ +server.port=9090 + spring.jpa.hibernate.ddl-auto=none 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 75% rename from src/main/resources/schema.sql rename to server/src/main/resources/schema.sql index 832ce04..7b83546 100644 --- a/src/main/resources/schema.sql +++ b/server/src/main/resources/schema.sql @@ -6,12 +6,22 @@ CREATE TABLE IF NOT EXISTS users ( CONSTRAINT UQ_USER_EMAIL UNIQUE (email) ); +CREATE TABLE IF NOT EXISTS item_requests( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + description TEXT NOT NULL, + requestor_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT pk_item_request PRIMARY KEY (id) +); + CREATE TABLE IF NOT EXISTS items ( id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, description VARCHAR(255) NOT NULL, available BOOLEAN NOT NULL, owner_id BIGINT NOT NULL, + request_id BIGINT, + FOREIGN KEY (request_id) REFERENCES item_requests(id) ON DELETE SET NULL, CONSTRAINT pk_item PRIMARY KEY (id), FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE ); @@ -34,6 +44,7 @@ CREATE TABLE IF NOT EXISTS comments ( author_id BIGINT NOT NULL, content TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT pk_comments PRIMARY KEY (id), FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE, FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE ); diff --git a/server/src/test/java/ru/practicum/shareit/Random.java b/server/src/test/java/ru/practicum/shareit/Random.java new file mode 100644 index 0000000..a982d3b --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/Random.java @@ -0,0 +1,64 @@ +package ru.practicum.shareit; + +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.item.dto.CommentCreate; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.request.dto.ItemRequestCreate; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +public class Random { + private static final String letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + private static final java.util.Random random = new java.util.Random(); + + + public static User getUser() { + User u = new User(); + u.setName(getRandomString()); + u.setEmail(getRandomEmail()); + + return u; + } + + public static ItemRequestCreate getItemRequest() { + return new ItemRequestCreate(getRandomString()); + } + + public static CommentCreate getComment() { + return new CommentCreate(getRandomString()); + } + + public static ItemDto getItemDto() { + ItemDto dto = new ItemDto(); + dto.setName(getRandomString()); + dto.setDescription(getRandomString()); + dto.setAvailable(random.nextBoolean()); + + return dto; + } + + public static BookingDto getBookingDto(Long itemId) { + BookingDto dto = new BookingDto(); + dto.setItemId(itemId); + dto.setStart(LocalDateTime.now().plusDays(1)); + dto.setEnd(LocalDateTime.now().plusDays(2)); + return dto; + } + + private static String getRandomString() { + int length = random.nextInt(10) + 1; + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < length; i++) { + int index = random.nextInt(letters.length()); + sb.append(letters.charAt(index)); + } + return sb.toString(); + } + + private static String getRandomEmail() { + return getRandomString() + "@" + getRandomString() + ".com"; + } + +} diff --git a/src/test/java/ru/practicum/shareit/ShareItTests.java b/server/src/test/java/ru/practicum/shareit/ShareItTests.java similarity index 85% rename from src/test/java/ru/practicum/shareit/ShareItTests.java rename to server/src/test/java/ru/practicum/shareit/ShareItTests.java index 4d79052..291f416 100644 --- a/src/test/java/ru/practicum/shareit/ShareItTests.java +++ b/server/src/test/java/ru/practicum/shareit/ShareItTests.java @@ -8,6 +8,7 @@ class ShareItTests { @Test void contextLoads() { + ShareItApp.main(new String[]{}); } } diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingControllerIntegrationTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingControllerIntegrationTest.java new file mode 100644 index 0000000..91ed1a5 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingControllerIntegrationTest.java @@ -0,0 +1,178 @@ +package ru.practicum.shareit.booking; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.Constants; +import ru.practicum.shareit.Random; +import ru.practicum.shareit.booking.dao.BookingRepository; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.enums.BookingStatus; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.item.dao.ItemRepository; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.dao.UserRepository; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +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; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +public class BookingControllerIntegrationTest { + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private BookingRepository bookingRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ItemRepository itemRepository; + + @Test + public void shouldCreateBooking() throws Exception { + User owner = userRepository.save(Random.getUser()); + User booker = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + + BookingDto dto = Random.getBookingDto(item.getId()); + String json = objectMapper.writeValueAsString(dto); + + mockMvc.perform(post("/bookings") + .header(Constants.USER_ID_HEADER, booker.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").exists()) + .andExpect(jsonPath("$.status").value("WAITING")); + + List bookings = bookingRepository.findByBookerIdOrderByEndDesc(booker.getId()); + assertThat(bookings).hasSize(1); + assertThat(bookings.getFirst().getStatus()).isEqualTo(BookingStatus.WAITING); + } + + @Test + public void shouldUpdateBookingStatus() throws Exception { + User owner = userRepository.save(Random.getUser()); + User booker = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + Booking booking = bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().plusDays(1)) + .end(LocalDateTime.now().plusDays(2)) + .item(item) + .booker(booker) + .status(BookingStatus.WAITING) + .build()); + + mockMvc.perform(patch("/bookings/{bookingId}", booking.getId()) + .header(Constants.USER_ID_HEADER, owner.getId()) + .param("approved", "true")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("APPROVED")); + + Booking updatedBooking = bookingRepository.findById(booking.getId()).orElseThrow(); + assertThat(updatedBooking.getStatus()).isEqualTo(BookingStatus.APPROVED); + } + + @Test + public void shouldGetBooking() throws Exception { + User owner = userRepository.save(Random.getUser()); + User booker = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + Booking booking = bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().plusDays(1)) + .end(LocalDateTime.now().plusDays(2)) + .item(item) + .booker(booker) + .status(BookingStatus.APPROVED) + .build()); + + mockMvc.perform(get("/bookings/{bookingId}", booking.getId()) + .header(Constants.USER_ID_HEADER, booker.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(booking.getId())); + } + + @Test + public void shouldFindByBookerAndState() throws Exception { + User owner = userRepository.save(Random.getUser()); + User booker = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + Booking booking = bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().plusDays(1)) + .end(LocalDateTime.now().plusDays(2)) + .item(item) + .booker(booker) + .status(BookingStatus.WAITING) + .build()); + + mockMvc.perform(get("/bookings") + .header(Constants.USER_ID_HEADER, booker.getId()) + .param("state", "WAITING")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(booking.getId())); + } + + @Test + public void shouldFindByOwnerAndState() throws Exception { + User owner = userRepository.save(Random.getUser()); + User booker = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + Booking booking = bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().plusDays(1)) + .end(LocalDateTime.now().plusDays(2)) + .item(item) + .booker(booker) + .status(BookingStatus.WAITING) + .build()); + + mockMvc.perform(get("/bookings/owner") + .header(Constants.USER_ID_HEADER, owner.getId()) + .param("state", "WAITING")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(booking.getId())); + } +} \ No newline at end of file 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..b982655 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java @@ -0,0 +1,114 @@ +package ru.practicum.shareit.booking; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.Constants; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.enums.BookingStatus; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.service.BookingService; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(BookingController.class) +public class BookingControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private BookingService bookingService; + + @Test + public void shouldCreateBooking() throws Exception { + BookingDto bookingDto = new BookingDto(); + bookingDto.setItemId(1L); + bookingDto.setStart(LocalDateTime.now().plusDays(1)); + bookingDto.setEnd(LocalDateTime.now().plusDays(2)); + + Booking booking = new Booking(); + booking.setId(1L); + booking.setStatus(BookingStatus.WAITING); + when(bookingService.createBooking(anyLong(), any())).thenReturn(booking); + + mockMvc.perform(post("/bookings") + .header(Constants.USER_ID_HEADER, 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(bookingDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.status").value("WAITING")); + } + + @Test + public void shouldUpdateBookingStatus() throws Exception { + Booking booking = new Booking(); + booking.setId(1L); + booking.setStatus(BookingStatus.APPROVED); + when(bookingService.updateBookingStatus(anyLong(), anyLong(), anyBoolean())).thenReturn(booking); + + mockMvc.perform(patch("/bookings/1") + .header(Constants.USER_ID_HEADER, 1L) + .param("approved", "true")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.status").value("APPROVED")); + } + + @Test + public void shouldGetBooking() throws Exception { + Booking booking = new Booking(); + booking.setId(1L); + booking.setItem(new Item()); + booking.setBooker(new User()); + when(bookingService.getBooking(anyLong(), anyLong())).thenReturn(booking); + + mockMvc.perform(get("/bookings/1") + .header(Constants.USER_ID_HEADER, 1L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)); + } + + @Test + public void shouldFindByBookerAndState() throws Exception { + Booking booking = new Booking(); + booking.setId(1L); + when(bookingService.findByBookerAndState(anyLong(), any())).thenReturn(Collections.singletonList(booking)); + + mockMvc.perform(get("/bookings") + .header(Constants.USER_ID_HEADER, 1L) + .param("state", "ALL")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)); + } + + @Test + public void shouldFindByOwnerAndState() throws Exception { + Booking booking = new Booking(); + booking.setId(1L); + when(bookingService.findByOwnerAndState(anyLong(), any())).thenReturn(Collections.singletonList(booking)); + + mockMvc.perform(get("/bookings/owner") + .header(Constants.USER_ID_HEADER, 1L) + .param("state", "ALL")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)); + } +} \ No newline at end of file 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..3285edb --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingMapperTest.java @@ -0,0 +1,26 @@ +package ru.practicum.shareit.booking; + +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.model.Booking; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class BookingMapperTest { + + @Test + public void shouldMapBookingDtoToBooking() { + BookingDto bookingDto = new BookingDto(); + bookingDto.setStart(LocalDateTime.of(2023, 1, 1, 10, 0)); + bookingDto.setEnd(LocalDateTime.of(2023, 1, 2, 10, 0)); + + Booking booking = BookingMapper.toBooking(bookingDto); + + assertNotNull(booking); + assertEquals(bookingDto.getStart(), booking.getStart()); + assertEquals(bookingDto.getEnd(), booking.getEnd()); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/booking/dao/BookingRepositoryTest.java b/server/src/test/java/ru/practicum/shareit/booking/dao/BookingRepositoryTest.java new file mode 100644 index 0000000..544c8cf --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/dao/BookingRepositoryTest.java @@ -0,0 +1,358 @@ +package ru.practicum.shareit.booking.dao; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import ru.practicum.shareit.Random; +import ru.practicum.shareit.booking.dto.LastAndNextDate; +import ru.practicum.shareit.booking.enums.BookingStatus; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.item.dao.ItemRepository; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.dao.UserRepository; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@DataJpaTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class BookingRepositoryTest { + + private final BookingRepository bookingRepository; + private final UserRepository userRepository; + private final ItemRepository itemRepository; + + @Test + public void shouldFindLastAndNextDatesByOwnerId() { + User owner = userRepository.save(Random.getUser()); + User booker = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + + Booking pastBooking = bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().minusDays(2)) + .end(LocalDateTime.now().minusDays(1)) + .item(item) + .booker(booker) + .status(BookingStatus.APPROVED) + .build()); + + List dates = bookingRepository.findLastAndNextDatesByOwnerId(owner.getId()); + + assertThat(dates).hasSize(1); + assertThat(dates.getFirst().getItemId()).isEqualTo(item.getId()); + } + + @Test + public void shouldExistsByItemIdAndBookerIdAndStatusIsAndEndBefore() { + User owner = userRepository.save(Random.getUser()); + User booker = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + + bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().minusDays(2)) + .end(LocalDateTime.now().minusDays(1)) + .item(item) + .booker(booker) + .status(BookingStatus.APPROVED) + .build()); + + Boolean exists = bookingRepository.existsByItemIdAndBookerIdAndStatusIsAndEndBefore( + item.getId(), + booker.getId(), + BookingStatus.APPROVED, + LocalDateTime.now() + ); + + assertThat(exists).isTrue(); + } + + @Test + public void shouldFindByBookerIdOrderByEndDesc() { + User booker = userRepository.save(Random.getUser()); + User owner = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + + Booking booking1 = bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().plusDays(1)) + .end(LocalDateTime.now().plusDays(2)) + .item(item) + .booker(booker) + .status(BookingStatus.APPROVED) + .build()); + + Booking booking2 = bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().plusDays(3)) + .end(LocalDateTime.now().plusDays(4)) + .item(item) + .booker(booker) + .status(BookingStatus.APPROVED) + .build()); + + List bookings = bookingRepository.findByBookerIdOrderByEndDesc(booker.getId()); + + assertThat(bookings).hasSize(2); + assertThat(bookings.getFirst().getEnd()).isAfter(bookings.get(1).getEnd()); + } + + @Test + public void shouldFindByBookerIdAndStatusIsOrderByEndDesc() { + User booker = userRepository.save(Random.getUser()); + User owner = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + + bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().plusDays(1)) + .end(LocalDateTime.now().plusDays(2)) + .item(item) + .booker(booker) + .status(BookingStatus.WAITING) + .build()); + + List bookings = bookingRepository.findByBookerIdAndStatusIsOrderByEndDesc(booker.getId(), BookingStatus.WAITING); + + assertThat(bookings).hasSize(1); + assertThat(bookings.getFirst().getStatus()).isEqualTo(BookingStatus.WAITING); + } + + @Test + public void shouldFindByBookerIdAndStartIsBeforeAndEndIsBeforeOrderByEndDesc() { + User booker = userRepository.save(Random.getUser()); + User owner = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + + bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().minusDays(2)) + .end(LocalDateTime.now().minusDays(1)) + .item(item) + .booker(booker) + .status(BookingStatus.APPROVED) + .build()); + + List bookings = bookingRepository.findByBookerIdAndStartIsBeforeAndEndIsBeforeOrderByEndDesc( + booker.getId(), + LocalDateTime.now(), + LocalDateTime.now() + ); + + assertThat(bookings).hasSize(1); + } + + @Test + public void shouldFindByBookerIdAndStartIsAfterOrderByEndDesc() { + User booker = userRepository.save(Random.getUser()); + User owner = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + + bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().plusDays(1)) + .end(LocalDateTime.now().plusDays(2)) + .item(item) + .booker(booker) + .status(BookingStatus.APPROVED) + .build()); + + List bookings = bookingRepository.findByBookerIdAndStartIsAfterOrderByEndDesc( + booker.getId(), + LocalDateTime.now() + ); + + assertThat(bookings).hasSize(1); + } + + @Test + public void shouldFindByBookerIdAndEndIsBeforeOrderByEndDesc() { + User booker = userRepository.save(Random.getUser()); + User owner = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + + bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().minusDays(2)) + .end(LocalDateTime.now().minusDays(1)) + .item(item) + .booker(booker) + .status(BookingStatus.APPROVED) + .build()); + + List bookings = bookingRepository.findByBookerIdAndEndIsBeforeOrderByEndDesc( + booker.getId(), + LocalDateTime.now() + ); + + assertThat(bookings).hasSize(1); + } + + @Test + public void shouldFindByItemOwnerIdOrderByEndDesc() { + User owner = userRepository.save(Random.getUser()); + User booker = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + + bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().plusDays(1)) + .end(LocalDateTime.now().plusDays(2)) + .item(item) + .booker(booker) + .status(BookingStatus.APPROVED) + .build()); + + List bookings = bookingRepository.findByItemOwnerIdOrderByEndDesc(owner.getId()); + + assertThat(bookings).hasSize(1); + } + + @Test + public void shouldFindByItemOwnerIdAndStatusIsOrderByEndDesc() { + User owner = userRepository.save(Random.getUser()); + User booker = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + + bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().plusDays(1)) + .end(LocalDateTime.now().plusDays(2)) + .item(item) + .booker(booker) + .status(BookingStatus.WAITING) + .build()); + + List bookings = bookingRepository.findByItemOwnerIdAndStatusIsOrderByEndDesc( + owner.getId(), + BookingStatus.WAITING + ); + + assertThat(bookings).hasSize(1); + assertThat(bookings.getFirst().getStatus()).isEqualTo(BookingStatus.WAITING); + } + + @Test + public void shouldFindByItemOwnerIdAndStartIsBeforeAndEndIsBeforeOrderByEndDesc() { + User owner = userRepository.save(Random.getUser()); + User booker = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + + bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().minusDays(2)) + .end(LocalDateTime.now().minusDays(1)) + .item(item) + .booker(booker) + .status(BookingStatus.APPROVED) + .build()); + + List bookings = bookingRepository.findByItemOwnerIdAndStartIsBeforeAndEndIsBeforeOrderByEndDesc( + owner.getId(), + LocalDateTime.now(), + LocalDateTime.now() + ); + + assertThat(bookings).hasSize(1); + } + + @Test + public void shouldFindByItemOwnerIdAndStartIsAfterOrderByEndDesc() { + User owner = userRepository.save(Random.getUser()); + User booker = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + + bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().plusDays(1)) + .end(LocalDateTime.now().plusDays(2)) + .item(item) + .booker(booker) + .status(BookingStatus.APPROVED) + .build()); + + List bookings = bookingRepository.findByItemOwnerIdAndStartIsAfterOrderByEndDesc( + owner.getId(), + LocalDateTime.now() + ); + + assertThat(bookings).hasSize(1); + } + + @Test + public void shouldFindByItemOwnerIdAndEndIsBeforeOrderByEndDesc() { + User owner = userRepository.save(Random.getUser()); + User booker = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + + bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().minusDays(2)) + .end(LocalDateTime.now().minusDays(1)) + .item(item) + .booker(booker) + .status(BookingStatus.APPROVED) + .build()); + + List bookings = bookingRepository.findByItemOwnerIdAndEndIsBeforeOrderByEndDesc( + owner.getId(), + LocalDateTime.now() + ); + + assertThat(bookings).hasSize(1); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/booking/service/BookingServiceImplTest.java b/server/src/test/java/ru/practicum/shareit/booking/service/BookingServiceImplTest.java new file mode 100644 index 0000000..9e5f5b3 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/service/BookingServiceImplTest.java @@ -0,0 +1,303 @@ +package ru.practicum.shareit.booking.service; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import ru.practicum.shareit.booking.dao.BookingRepository; +import ru.practicum.shareit.booking.dto.BookingDto; +import ru.practicum.shareit.booking.enums.BookingState; +import ru.practicum.shareit.booking.enums.BookingStatus; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.item.dao.ItemRepository; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.dao.UserRepository; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.validation.exceptions.BadRequestException; +import ru.practicum.shareit.validation.exceptions.ForbiddenException; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class BookingServiceImplTest { + + @Mock + private BookingRepository bookingRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private ItemRepository itemRepository; + + @InjectMocks + private BookingServiceImpl bookingService; + + private final LocalDateTime now = LocalDateTime.now(); + + @Test + public void shouldCreateBooking() { + BookingDto bookingDto = new BookingDto(); + bookingDto.setItemId(1L); + bookingDto.setStart(now.plusDays(1)); + bookingDto.setEnd(now.plusDays(2)); + + User user = new User(1L, "booker", "booker@test.com"); + Item item = new Item(1L, "item", "desc", true, 2L, null); + Booking savedBooking = new Booking(); + savedBooking.setId(1L); + savedBooking.setStatus(BookingStatus.WAITING); + + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + when(itemRepository.findById(1L)).thenReturn(Optional.of(item)); + when(bookingRepository.save(any())).thenReturn(savedBooking); + + Booking result = bookingService.createBooking(1L, bookingDto); + + assertThat(result.getId()).isEqualTo(1L); + assertThat(result.getStatus()).isEqualTo(BookingStatus.WAITING); + verify(bookingRepository).save(any()); + } + + @Test + public void shouldThrowBadRequestExceptionWhenCreateBookingWithInvalidDates() { + BookingDto bookingDto = new BookingDto(); + bookingDto.setItemId(1L); + bookingDto.setStart(now.plusDays(2)); + bookingDto.setEnd(now.plusDays(1)); + + assertThatThrownBy(() -> bookingService.createBooking(1L, bookingDto)) + .isInstanceOf(BadRequestException.class); + } + + @Test + public void shouldThrowBadRequestExceptionWhenCreateBookingForUnavailableItem() { + BookingDto bookingDto = new BookingDto(); + bookingDto.setItemId(1L); + bookingDto.setStart(now.plusDays(1)); + bookingDto.setEnd(now.plusDays(2)); + + User user = new User(1L, "booker", "booker@test.com"); + Item item = new Item(1L, "item", "desc", false, 2L, null); + + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + when(itemRepository.findById(1L)).thenReturn(Optional.of(item)); + + assertThatThrownBy(() -> bookingService.createBooking(1L, bookingDto)) + .isInstanceOf(BadRequestException.class); + } + + @Test + public void shouldUpdateBookingStatus() { + User owner = new User(2L, "owner", "owner@test.com"); + Item item = new Item(1L, "item", "desc", true, 2L, null); + Booking booking = new Booking(); + booking.setId(1L); + booking.setItem(item); + booking.setBooker(new User(1L, "booker", "booker@test.com")); + booking.setStatus(BookingStatus.WAITING); + + when(bookingRepository.findById(1L)).thenReturn(Optional.of(booking)); + when(bookingRepository.save(any())).thenReturn(booking); + + Booking result = bookingService.updateBookingStatus(2L, 1L, true); + + assertThat(result.getStatus()).isEqualTo(BookingStatus.APPROVED); + verify(bookingRepository).save(any()); + } + + @Test + public void shouldThrowForbiddenExceptionWhenUpdateBookingStatusByNonOwner() { + Item item = new Item(1L, "item", "desc", true, 2L, null); + Booking booking = new Booking(); + booking.setItem(item); + booking.setBooker(new User(1L, "booker", "booker@test.com")); + + when(bookingRepository.findById(1L)).thenReturn(Optional.of(booking)); + + assertThatThrownBy(() -> bookingService.updateBookingStatus(999L, 1L, true)) + .isInstanceOf(ForbiddenException.class); + } + + @Test + public void shouldGetBooking() { + User booker = new User(1L, "booker", "booker@test.com"); + Item item = new Item(1L, "item", "desc", true, 2L, null); + Booking booking = new Booking(); + booking.setId(1L); + booking.setItem(item); + booking.setBooker(booker); + + when(bookingRepository.findById(1L)).thenReturn(Optional.of(booking)); + + Booking result = bookingService.getBooking(1L, 1L); + + assertThat(result.getId()).isEqualTo(1L); + verify(bookingRepository).findById(1L); + } + + @Test + public void shouldThrowForbiddenExceptionWhenGetBookingByUnauthorizedUser() { + User booker = new User(1L, "booker", "booker@test.com"); + Item item = new Item(1L, "item", "desc", true, 2L, null); + Booking booking = new Booking(); + booking.setItem(item); + booking.setBooker(booker); + + when(bookingRepository.findById(1L)).thenReturn(Optional.of(booking)); + + assertThatThrownBy(() -> bookingService.getBooking(999L, 1L)) + .isInstanceOf(ForbiddenException.class); + } + + @Test + public void shouldFindByBookerAndStateAll() { + when(userRepository.existsById(1L)).thenReturn(true); + when(bookingRepository.findByBookerIdOrderByEndDesc(1L)).thenReturn(List.of(new Booking())); + + Collection result = bookingService.findByBookerAndState(1L, BookingState.ALL); + + assertThat(result).hasSize(1); + verify(bookingRepository).findByBookerIdOrderByEndDesc(1L); + } + + @Test + public void shouldFindByOwnerAndStateAll() { + when(userRepository.existsById(1L)).thenReturn(true); + when(bookingRepository.findByItemOwnerIdOrderByEndDesc(1L)).thenReturn(List.of(new Booking())); + + Collection result = bookingService.findByOwnerAndState(1L, BookingState.ALL); + + assertThat(result).hasSize(1); + verify(bookingRepository).findByItemOwnerIdOrderByEndDesc(1L); + } + + @Test + public void shouldFindByBookerAndStateRejected() { + when(userRepository.existsById(1L)).thenReturn(true); + when(bookingRepository.findByBookerIdAndStatusIsOrderByEndDesc(1L, BookingStatus.REJECTED)).thenReturn(List.of(new Booking())); + + Collection result = bookingService.findByBookerAndState(1L, BookingState.REJECTED); + + assertThat(result).hasSize(1); + verify(bookingRepository).findByBookerIdAndStatusIsOrderByEndDesc(1L, BookingStatus.REJECTED); + } + + @Test + public void shouldFindByOwnerAndStateRejected() { + when(userRepository.existsById(1L)).thenReturn(true); + when(bookingRepository.findByItemOwnerIdAndStatusIsOrderByEndDesc(1L, BookingStatus.REJECTED)).thenReturn(List.of(new Booking())); + + Collection result = bookingService.findByOwnerAndState(1L, BookingState.REJECTED); + + assertThat(result).hasSize(1); + verify(bookingRepository).findByItemOwnerIdAndStatusIsOrderByEndDesc(1L, BookingStatus.REJECTED); + } + + @Test + public void shouldFindByBookerAndStateWaiting() { + when(userRepository.existsById(1L)).thenReturn(true); + when(bookingRepository.findByBookerIdAndStatusIsOrderByEndDesc(1L, BookingStatus.WAITING)) + .thenReturn(List.of(new Booking())); + + Collection result = bookingService.findByBookerAndState(1L, BookingState.WAITING); + + assertThat(result).hasSize(1); + verify(bookingRepository).findByBookerIdAndStatusIsOrderByEndDesc(1L, BookingStatus.WAITING); + } + + @Test + public void shouldFindByOwnerAndStateWaiting() { + when(userRepository.existsById(1L)).thenReturn(true); + when(bookingRepository.findByItemOwnerIdAndStatusIsOrderByEndDesc(1L, BookingStatus.WAITING)) + .thenReturn(List.of(new Booking())); + + Collection result = bookingService.findByOwnerAndState(1L, BookingState.WAITING); + + assertThat(result).hasSize(1); + verify(bookingRepository).findByItemOwnerIdAndStatusIsOrderByEndDesc(1L, BookingStatus.WAITING); + } + + @Test + public void shouldFindByBookerAndStateCurrent() { + when(userRepository.existsById(1L)).thenReturn(true); + when(bookingRepository.findByBookerIdAndStartIsBeforeAndEndIsBeforeOrderByEndDesc(any(), any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(List.of(new Booking())); + + Collection result = bookingService.findByBookerAndState(1L, BookingState.CURRENT); + + assertThat(result).hasSize(1); + verify(bookingRepository).findByBookerIdAndStartIsBeforeAndEndIsBeforeOrderByEndDesc(any(), any(LocalDateTime.class), any(LocalDateTime.class)); + } + + @Test + public void shouldFindByOwnerAndStateCurrent() { + when(userRepository.existsById(1L)).thenReturn(true); + + when(bookingRepository.findByItemOwnerIdAndStartIsBeforeAndEndIsBeforeOrderByEndDesc(any(), any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(List.of(new Booking())); + + Collection result = bookingService.findByOwnerAndState(1L, BookingState.CURRENT); + + assertThat(result).hasSize(1); + verify(bookingRepository).findByItemOwnerIdAndStartIsBeforeAndEndIsBeforeOrderByEndDesc(any(), any(LocalDateTime.class), any(LocalDateTime.class)); + } + + @Test + public void shouldFindByBookerAndStatePast() { + when(userRepository.existsById(1L)).thenReturn(true); + when(bookingRepository.findByBookerIdAndEndIsBeforeOrderByEndDesc(any(), any(LocalDateTime.class))) + .thenReturn(List.of(new Booking())); + + Collection result = bookingService.findByBookerAndState(1L, BookingState.PAST); + + assertThat(result).hasSize(1); + verify(bookingRepository).findByBookerIdAndEndIsBeforeOrderByEndDesc(any(), any(LocalDateTime.class)); + } + + @Test + public void shouldFindByOwnerAndStatePast() { + when(userRepository.existsById(1L)).thenReturn(true); + when(bookingRepository.findByItemOwnerIdAndEndIsBeforeOrderByEndDesc(any(), any(LocalDateTime.class))) + .thenReturn(List.of(new Booking())); + + Collection result = bookingService.findByOwnerAndState(1L, BookingState.PAST); + + assertThat(result).hasSize(1); + verify(bookingRepository).findByItemOwnerIdAndEndIsBeforeOrderByEndDesc(any(), any(LocalDateTime.class)); + } + + @Test + public void shouldFindByBookerAndStateFuture() { + when(userRepository.existsById(1L)).thenReturn(true); + when(bookingRepository.findByBookerIdAndStartIsAfterOrderByEndDesc(any(), any(LocalDateTime.class))) + .thenReturn(List.of(new Booking())); + + Collection result = bookingService.findByBookerAndState(1L, BookingState.FUTURE); + + assertThat(result).hasSize(1); + verify(bookingRepository).findByBookerIdAndStartIsAfterOrderByEndDesc(any(), any(LocalDateTime.class)); + } + + @Test + public void shouldFindByOwnerAndStateFuture() { + when(userRepository.existsById(1L)).thenReturn(true); + when(bookingRepository.findByItemOwnerIdAndStartIsAfterOrderByEndDesc(any(), any(LocalDateTime.class))) + .thenReturn(List.of(new Booking())); + + Collection result = bookingService.findByOwnerAndState(1L, BookingState.FUTURE); + + assertThat(result).hasSize(1); + verify(bookingRepository).findByItemOwnerIdAndStartIsAfterOrderByEndDesc(any(), any(LocalDateTime.class)); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemControllerIntegrationTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemControllerIntegrationTest.java new file mode 100644 index 0000000..fa0ce28 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/ItemControllerIntegrationTest.java @@ -0,0 +1,177 @@ +package ru.practicum.shareit.item; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.Constants; +import ru.practicum.shareit.Random; +import ru.practicum.shareit.booking.dao.BookingRepository; +import ru.practicum.shareit.booking.enums.BookingStatus; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.item.dao.CommentRepository; +import ru.practicum.shareit.item.dao.ItemRepository; +import ru.practicum.shareit.item.dto.CommentCreate; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.dao.UserRepository; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.Collection; + +import static org.assertj.core.api.Assertions.assertThat; +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; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +public class ItemControllerIntegrationTest { + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ItemRepository itemRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private BookingRepository bookingRepository; + + @Autowired + private CommentRepository commentRepository; + + @Test + public void shouldGetItem() throws Exception { + User owner = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + + mockMvc.perform(get("/items/{itemId}", item.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(item.getId())) + .andExpect(jsonPath("$.name").value("item")); + } + + @Test + public void shouldSearchItems() throws Exception { + User owner = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("searchable") + .description("description") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + + mockMvc.perform(get("/items/search") + .param("text", "search")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(item.getId())); + } + + @Test + public void shouldGetItems() throws Exception { + User owner = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + + mockMvc.perform(get("/items") + .header(Constants.USER_ID_HEADER, owner.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(item.getId())); + } + + @Test + public void shouldAddItem() throws Exception { + User owner = userRepository.save(User.builder().name("owner3").email("owner3@email.com").build()); + ItemDto dto = Random.getItemDto(); + String json = objectMapper.writeValueAsString(dto); + + mockMvc.perform(post("/items") + .header(Constants.USER_ID_HEADER, owner.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value(dto.getName())); + + Collection items = itemRepository.findByOwnerId(owner.getId()); + assertThat(items).hasSize(1); + assertThat(items.iterator().next().getName()).isEqualTo(dto.getName()); + } + + @Test + public void shouldUpdateItem() throws Exception { + User owner = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("old") + .description("old desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + ItemDto dto = Random.getItemDto(); + + String json = objectMapper.writeValueAsString(dto); + + mockMvc.perform(patch("/items/{itemId}", item.getId()) + .header(Constants.USER_ID_HEADER, owner.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value(dto.getName())); + + Item updatedItem = itemRepository.findById(item.getId()).orElseThrow(); + assertThat(updatedItem.getName()).isEqualTo(dto.getName()); + assertThat(updatedItem.getDescription()).isEqualTo(dto.getDescription()); + } + + @Test + public void shouldAddComment() throws Exception { + User owner = userRepository.save(Random.getUser()); + User booker = userRepository.save(Random.getUser()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(owner.getId()) + .build()); + Booking booking = bookingRepository.save(Booking.builder() + .start(LocalDateTime.now().minusDays(2)) + .end(LocalDateTime.now().minusDays(1)) + .item(item) + .booker(booker) + .status(BookingStatus.APPROVED) + .build()); + + CommentCreate cc = Random.getComment(); + + String json = objectMapper.writeValueAsString(cc); + + mockMvc.perform(post("/items/{itemId}/comment", item.getId()) + .header(Constants.USER_ID_HEADER, booker.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.text").value(cc.text())); + + assertThat(commentRepository.findByItemId(item.getId())).hasSize(1); + } +} \ No newline at end of file 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..35e1874 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/ItemControllerTest.java @@ -0,0 +1,128 @@ +package ru.practicum.shareit.item; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.Constants; +import ru.practicum.shareit.item.dto.CommentCreate; +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.service.ItemService; + +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(ItemController.class) +public class ItemControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private ItemService itemService; + + @Test + public void shouldGetItem() throws Exception { + ItemDto dto = new ItemDto(); + dto.setId(1L); + dto.setName("item"); + when(itemService.getItem(1L)).thenReturn(dto); + + mockMvc.perform(get("/items/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("item")); + } + + @Test + public void shouldSearchItems() throws Exception { + ItemDto dto = new ItemDto(); + dto.setId(1L); + when(itemService.searchItems("text")).thenReturn(Collections.singletonList(dto)); + + mockMvc.perform(get("/items/search") + .param("text", "text")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)); + } + + @Test + public void shouldGetItems() throws Exception { + ItemDto dto = new ItemDto(); + dto.setId(1L); + when(itemService.getItems(1L)).thenReturn(Collections.singletonList(dto)); + + mockMvc.perform(get("/items") + .header(Constants.USER_ID_HEADER, 1L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)); + } + + @Test + public void shouldAddItem() throws Exception { + ItemDto itemDto = new ItemDto(); + itemDto.setName("item"); + itemDto.setDescription("desc"); + itemDto.setAvailable(true); + + ItemDto savedDto = new ItemDto(); + savedDto.setId(1L); + when(itemService.addItem(anyLong(), any())).thenReturn(savedDto); + + mockMvc.perform(post("/items") + .header(Constants.USER_ID_HEADER, 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(itemDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)); + } + + @Test + public void shouldUpdateItem() throws Exception { + ItemDto itemDto = new ItemDto(); + itemDto.setName("updated"); + + ItemDto updatedDto = new ItemDto(); + updatedDto.setId(1L); + updatedDto.setName("updated"); + when(itemService.updateItem(anyLong(), anyLong(), any())).thenReturn(updatedDto); + + mockMvc.perform(patch("/items/1") + .header(Constants.USER_ID_HEADER, 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(itemDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("updated")); + } + + @Test + public void shouldAddComment() throws Exception { + CommentCreate commentCreate = new CommentCreate("comment"); + CommentDto commentDto = new CommentDto(); + commentDto.setId(1L); + commentDto.setText("comment"); + when(itemService.addComment(anyLong(), anyLong(), any())).thenReturn(commentDto); + + mockMvc.perform(post("/items/1/comment") + .header(Constants.USER_ID_HEADER, 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(commentCreate))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.text").value("comment")); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/dao/CommentRepositoryTest.java b/server/src/test/java/ru/practicum/shareit/item/dao/CommentRepositoryTest.java new file mode 100644 index 0000000..9b7e8c6 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/dao/CommentRepositoryTest.java @@ -0,0 +1,92 @@ +package ru.practicum.shareit.item.dao; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import ru.practicum.shareit.Random; +import ru.practicum.shareit.item.mappers.ItemMapper; +import ru.practicum.shareit.item.model.Comment; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.dao.UserRepository; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.Collection; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@DataJpaTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class CommentRepositoryTest { + + private final CommentRepository commentRepository; + private final ItemRepository itemRepository; + private final UserRepository userRepository; + + @Test + public void shouldFindByItemId() { + User user = Random.getUser(); + user = userRepository.save(user); + + Item item = ItemMapper.toItem(Random.getItemDto()); + item.setOwnerId(user.getId()); + + item = itemRepository.save(item); + + User author = Random.getUser(); + author = userRepository.save(author); + + Comment comment = new Comment(); + comment.setAuthor(author); + comment.setItem(item); + comment.setText(Random.getComment().text()); + comment.setCreated(LocalDateTime.now()); + + commentRepository.save(comment); + + Collection comments = commentRepository.findByItemId(item.getId()); + + assertThat(comments).hasSize(1); + + Comment c = comments.iterator().next(); + + assertThat(c.getId()).isEqualTo(comment.getId()); + assertThat(c.getAuthor()).isEqualTo(author); + assertThat(c.getItem()).isEqualTo(item); + } + + @Test + public void shouldFindByItemOwnerId() { + User user = Random.getUser(); + user = userRepository.save(user); + + Item item = ItemMapper.toItem(Random.getItemDto()); + item.setOwnerId(user.getId()); + + item = itemRepository.save(item); + + User author = Random.getUser(); + author = userRepository.save(author); + + Comment comment = new Comment(); + comment.setAuthor(author); + comment.setItem(item); + comment.setText(Random.getComment().text()); + comment.setCreated(LocalDateTime.now()); + + commentRepository.save(comment); + + Collection comments = commentRepository.findByItemOwnerId(item.getOwnerId()); + + assertThat(comments).hasSize(1); + + Comment c = comments.iterator().next(); + + assertThat(c.getId()).isEqualTo(comment.getId()); + assertThat(c.getAuthor()).isEqualTo(author); + assertThat(c.getItem()).isEqualTo(item); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/dao/ItemRepositoryTest.java b/server/src/test/java/ru/practicum/shareit/item/dao/ItemRepositoryTest.java new file mode 100644 index 0000000..096cfb2 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/dao/ItemRepositoryTest.java @@ -0,0 +1,119 @@ +package ru.practicum.shareit.item.dao; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import ru.practicum.shareit.Random; +import ru.practicum.shareit.item.dto.ItemShortWithRequestId; +import ru.practicum.shareit.item.mappers.ItemMapper; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.request.dao.ItemRequestRepository; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.dao.UserRepository; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@DataJpaTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class ItemRepositoryTest { + + private final ItemRepository itemRepository; + private final UserRepository userRepository; + private final ItemRequestRepository itemRequestRepository; + + @Test + public void shouldFindByOwnerId() { + User user = userRepository.save(Random.getUser()); + + Item item1 = ItemMapper.toItem(Random.getItemDto()); + item1.setOwnerId(user.getId()); + item1 = itemRepository.save(item1); + + Item item2 = ItemMapper.toItem(Random.getItemDto()); + item2.setOwnerId(user.getId()); + item2 = itemRepository.save(item2); + + Collection items = itemRepository.findByOwnerId(user.getId()); + + assertThat(items).hasSize(2); + assertThat(items).extracting(Item::getId).containsExactlyInAnyOrder(item1.getId(), item2.getId()); + } + + @Test + public void shouldFindByRequestId() { + User user = userRepository.save(Random.getUser()); + + ItemRequest ir = new ItemRequest(); + ir.setRequestorId(user.getId()); + ir.setDescription(Random.getItemRequest().description()); + ir.setCreated(LocalDateTime.now()); + ir = itemRequestRepository.save(ir); + + User user1 = userRepository.save(Random.getUser()); + Item item1 = ItemMapper.toItem(Random.getItemDto()); + item1.setOwnerId(user1.getId()); + item1.setRequestId(ir.getId()); + item1 = itemRepository.save(item1); + + Collection items = itemRepository.findByRequestId(ir.getId()); + + assertThat(items).hasSize(1); + assertThat(items.iterator().next().getId()).isEqualTo(item1.getId()); + } + + @Test + public void shouldFindAllByRequestIdIn() { + User user = userRepository.save(Random.getUser()); + + ItemRequest ir1 = new ItemRequest(); + ir1.setRequestorId(user.getId()); + ir1.setDescription(Random.getItemRequest().description()); + ir1.setCreated(LocalDateTime.now()); + ir1 = itemRequestRepository.save(ir1); + + ItemRequest ir2 = new ItemRequest(); + ir2.setRequestorId(user.getId()); + ir2.setDescription(Random.getItemRequest().description()); + ir2.setCreated(LocalDateTime.now()); + ir2 = itemRequestRepository.save(ir2); + + User user1 = userRepository.save(Random.getUser()); + Item item1 = ItemMapper.toItem(Random.getItemDto()); + item1.setOwnerId(user1.getId()); + item1.setRequestId(ir1.getId()); + item1 = itemRepository.save(item1); + + Item item2 = ItemMapper.toItem(Random.getItemDto()); + item2.setOwnerId(user1.getId()); + item2.setRequestId(ir2.getId()); + item2 = itemRepository.save(item2); + + Collection items = itemRepository.findAllByRequestIdIn(List.of(ir1.getId(), ir2.getId())); + + assertThat(items).hasSize(2); + assertThat(items).extracting(ItemShortWithRequestId::getId).containsExactlyInAnyOrder(item1.getId(), item2.getId()); + } + + @Test + public void shouldSearchByDescription() { + User user = userRepository.save(Random.getUser()); + + Item item1 = ItemMapper.toItem(Random.getItemDto()); + item1.setOwnerId(user.getId()); + item1.setDescription("SPEC_DESCRIPTION"); + item1 = itemRepository.save(item1); + + Collection items = itemRepository.searchByNameOrDescription(item1.getDescription()); + + assertThat(items).hasSize(1); + assertThat(items.iterator().next().getId()).isEqualTo(item1.getId()); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/mappers/CommentMapperTest.java b/server/src/test/java/ru/practicum/shareit/item/mappers/CommentMapperTest.java new file mode 100644 index 0000000..4dfd60a --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/mappers/CommentMapperTest.java @@ -0,0 +1,37 @@ +package ru.practicum.shareit.item.mappers; + +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.item.dto.CommentDto; +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.assertEquals; + +public class CommentMapperTest { + + @Test + public void shouldMapCommentToCommentDto() { + User author = new User(); + author.setName("Author Name"); + Item item = new Item(); + item.setId(1L); + Comment comment = new Comment(); + comment.setId(100L); + comment.setItem(item); + comment.setAuthor(author); + comment.setText("Comment text"); + LocalDateTime created = LocalDateTime.now(); + comment.setCreated(created); + + CommentDto commentDto = CommentMapper.toCommentDto(comment); + + assertEquals(comment.getId(), commentDto.getId()); + assertEquals(comment.getItem().getId(), commentDto.getItemId()); + assertEquals(comment.getAuthor().getName(), commentDto.getAuthorName()); + assertEquals(comment.getText(), commentDto.getText()); + assertEquals(comment.getCreated(), commentDto.getCreated()); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/mappers/ItemMapperTest.java b/server/src/test/java/ru/practicum/shareit/item/mappers/ItemMapperTest.java new file mode 100644 index 0000000..bc1c657 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/mappers/ItemMapperTest.java @@ -0,0 +1,144 @@ +package ru.practicum.shareit.item.mappers; + +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.model.Item; + +import static org.junit.jupiter.api.Assertions.*; + +public class ItemMapperTest { + + @Test + public void shouldMapItemDtoToItem() { + ItemDto itemDto = new ItemDto(); + itemDto.setId(1L); + itemDto.setName("Item name"); + itemDto.setDescription("Item description"); + itemDto.setAvailable(true); + itemDto.setRequestId(10L); + + Item item = ItemMapper.toItem(itemDto); + + assertEquals(itemDto.getId(), item.getId()); + assertEquals(itemDto.getName(), item.getName()); + assertEquals(itemDto.getDescription(), item.getDescription()); + assertEquals(itemDto.getAvailable(), item.getIsAvailable()); + assertEquals(itemDto.getRequestId(), item.getRequestId()); + } + + @Test + public void shouldMapItemToItemDto() { + Item item = new Item(); + item.setId(2L); + item.setName("Item"); + item.setDescription("Description"); + item.setIsAvailable(false); + + ItemDto itemDto = ItemMapper.toItemDto(item); + + assertEquals(item.getId(), itemDto.getId()); + assertEquals(item.getName(), itemDto.getName()); + assertEquals(item.getDescription(), itemDto.getDescription()); + assertEquals(item.getIsAvailable(), itemDto.getAvailable()); + assertNull(itemDto.getRequestId()); + } + + @Test + public void shouldMergeItemWhenAllFieldsNonNull() { + Item item = new Item(); + item.setName("Old name"); + item.setDescription("Old description"); + item.setIsAvailable(false); + item.setRequestId(5L); + + ItemDto itemDto = new ItemDto(); + itemDto.setName("New name"); + itemDto.setDescription("New description"); + itemDto.setAvailable(true); + itemDto.setRequestId(10L); + + Item merged = ItemMapper.merge(item, itemDto); + + assertEquals(itemDto.getName(), merged.getName()); + assertEquals(itemDto.getDescription(), merged.getDescription()); + assertEquals(itemDto.getAvailable(), merged.getIsAvailable()); + assertEquals(itemDto.getRequestId(), merged.getRequestId()); + } + + @Test + public void shouldMergeItemWhenOnlyNameNotNull() { + Item item = new Item(); + item.setName("Old name"); + item.setDescription("Old description"); + item.setIsAvailable(true); + item.setRequestId(5L); + + ItemDto itemDto = new ItemDto(); + itemDto.setName("New name"); + + Item merged = ItemMapper.merge(item, itemDto); + + assertEquals(itemDto.getName(), merged.getName()); + assertEquals("Old description", merged.getDescription()); + assertTrue(merged.getIsAvailable()); + assertEquals(5L, merged.getRequestId()); + } + + @Test + public void shouldMergeItemWhenOnlyDescriptionNotNull() { + Item item = new Item(); + item.setName("Old name"); + item.setDescription("Old description"); + item.setIsAvailable(true); + item.setRequestId(5L); + + ItemDto itemDto = new ItemDto(); + itemDto.setDescription("New description"); + + Item merged = ItemMapper.merge(item, itemDto); + + assertEquals("Old name", merged.getName()); + assertEquals(itemDto.getDescription(), merged.getDescription()); + assertTrue(merged.getIsAvailable()); + assertEquals(5L, merged.getRequestId()); + } + + @Test + public void shouldMergeItemWhenOnlyAvailableNotNull() { + Item item = new Item(); + item.setName("Old name"); + item.setDescription("Old description"); + item.setIsAvailable(true); + item.setRequestId(5L); + + ItemDto itemDto = new ItemDto(); + itemDto.setAvailable(false); + + Item merged = ItemMapper.merge(item, itemDto); + + assertEquals("Old name", merged.getName()); + assertEquals("Old description", merged.getDescription()); + assertEquals(itemDto.getAvailable(), merged.getIsAvailable()); + assertEquals(5L, merged.getRequestId()); + } + + @Test + public void shouldMergeItemWhenOnlyRequestIdNotNull() { + Item item = new Item(); + item.setName("Old name"); + item.setDescription("Old description"); + item.setIsAvailable(true); + item.setRequestId(5L); + + ItemDto itemDto = new ItemDto(); + itemDto.setRequestId(15L); + + Item merged = ItemMapper.merge(item, itemDto); + + assertEquals("Old name", merged.getName()); + assertEquals("Old description", merged.getDescription()); + assertTrue(merged.getIsAvailable()); + assertEquals(itemDto.getRequestId(), merged.getRequestId()); + } + +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/service/ItemServiceImplTest.java b/server/src/test/java/ru/practicum/shareit/item/service/ItemServiceImplTest.java new file mode 100644 index 0000000..76c355d --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/service/ItemServiceImplTest.java @@ -0,0 +1,192 @@ +package ru.practicum.shareit.item.service; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import ru.practicum.shareit.booking.dao.BookingRepository; +import ru.practicum.shareit.item.dao.CommentRepository; +import ru.practicum.shareit.item.dao.ItemRepository; +import ru.practicum.shareit.item.dto.CommentCreate; +import ru.practicum.shareit.item.dto.CommentDto; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.model.Comment; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.service.UserService; +import ru.practicum.shareit.validation.exceptions.BadRequestException; +import ru.practicum.shareit.validation.exceptions.NotFoundException; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class ItemServiceImplTest { + + @Mock + private ItemRepository itemRepository; + + @Mock + private CommentRepository commentRepository; + + @Mock + private BookingRepository bookingRepository; + + @Mock + private UserService userService; + + @InjectMocks + private ItemServiceImpl itemService; + + @Test + public void shouldGetItem() { + Item item = new Item(1L, "name", "desc", true, 1L, null); + when(itemRepository.findById(1L)).thenReturn(Optional.of(item)); + when(commentRepository.findByItemId(1L)).thenReturn(List.of()); + + ItemDto result = itemService.getItem(1L); + + assertThat(result.getId()).isEqualTo(1L); + assertThat(result.getName()).isEqualTo("name"); + verify(itemRepository).findById(1L); + } + + @Test + public void shouldThrowNotFoundExceptionWhenGetItemWithInvalidId() { + when(itemRepository.findById(999L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> itemService.getItem(999L)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("Предмет с id 999 не найден"); + } + + @Test + public void shouldSearchItems() { + Item item = new Item(1L, "item", "description", true, 1L, null); + when(itemRepository.searchByNameOrDescription("desc")).thenReturn(List.of(item)); + + Collection result = itemService.searchItems("desc"); + + assertThat(result).hasSize(1); + assertThat(result.iterator().next().getName()).isEqualTo("item"); + verify(itemRepository).searchByNameOrDescription("desc"); + } + + @Test + public void shouldReturnEmptyListWhenSearchTextIsBlank() { + Collection result = itemService.searchItems(" "); + + assertThat(result).isEmpty(); + verify(itemRepository, never()).searchByNameOrDescription(any()); + } + + @Test + public void shouldGetItems() { + Item item = new Item(1L, "name", "desc", true, 1L, null); + doNothing().when(userService).throwIfUserNotFound(1L); + + when(itemRepository.findByOwnerId(1L)).thenReturn(List.of(item)); + when(bookingRepository.findLastAndNextDatesByOwnerId(1L)).thenReturn(List.of()); + when(commentRepository.findByItemOwnerId(1L)).thenReturn(List.of()); + + Collection result = itemService.getItems(1L); + + assertThat(result).hasSize(1); + verify(itemRepository).findByOwnerId(1L); + } + + @Test + public void shouldAddItem() { + ItemDto itemDto = new ItemDto(); + itemDto.setName("item"); + itemDto.setDescription("desc"); + itemDto.setAvailable(true); + + Item savedItem = new Item(1L, "item", "desc", true, 1L, null); + doNothing().when(userService).throwIfUserNotFound(1L); + + when(itemRepository.save(any())).thenReturn(savedItem); + + ItemDto result = itemService.addItem(1L, itemDto); + + assertThat(result.getId()).isEqualTo(1L); + verify(itemRepository).save(any()); + } + + @Test + public void shouldUpdateItem() { + Item currentItem = new Item(1L, "old", "old", true, 1L, null); + ItemDto updateDto = new ItemDto(); + updateDto.setName("new"); + updateDto.setDescription("new"); + + doNothing().when(userService).throwIfUserNotFound(1L); + + when(itemRepository.findById(1L)).thenReturn(Optional.of(currentItem)); + when(itemRepository.save(any())).thenReturn(currentItem); + + ItemDto result = itemService.updateItem(1L, 1L, updateDto); + + verify(itemRepository).save(any()); + } + + @Test + public void shouldThrowNotFoundExceptionWhenUpdateNonExistentItem() { + ItemDto updateDto = new ItemDto(); + doNothing().when(userService).throwIfUserNotFound(1L); + + when(itemRepository.findById(999L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> itemService.updateItem(1L, 999L, updateDto)) + .isInstanceOf(NotFoundException.class); + } + + @Test + public void shouldAddComment() { + User user = new User(1L, "name", "email@test.com"); + Item item = new Item(1L, "item", "desc", true, 2L, null); + CommentCreate commentCreate = new CommentCreate("comment"); + Comment comment = Comment.builder() + .id(1L) + .text("comment") + .author(user) + .item(item) + .created(LocalDateTime.now()) + .build(); + + when(userService.getUser(1L)).thenReturn(user); + when(itemRepository.findById(1L)).thenReturn(Optional.of(item)); + when(bookingRepository.existsByItemIdAndBookerIdAndStatusIsAndEndBefore(anyLong(), anyLong(), any(), any())) + .thenReturn(true); + when(commentRepository.save(any())).thenReturn(comment); + + CommentDto result = itemService.addComment(1L, 1L, commentCreate); + + assertThat(result.getText()).isEqualTo("comment"); + verify(commentRepository).save(any()); + } + + @Test + public void shouldThrowBadRequestExceptionWhenAddCommentWithoutBooking() { + User user = new User(1L, "name", "email@test.com"); + Item item = new Item(1L, "item", "desc", true, 2L, null); + CommentCreate commentCreate = new CommentCreate("comment"); + + when(userService.getUser(1L)).thenReturn(user); + when(itemRepository.findById(1L)).thenReturn(Optional.of(item)); + when(bookingRepository.existsByItemIdAndBookerIdAndStatusIsAndEndBefore(anyLong(), anyLong(), any(), any())) + .thenReturn(false); + + assertThatThrownBy(() -> itemService.addComment(1L, 1L, commentCreate)) + .isInstanceOf(BadRequestException.class); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/request/ItemRequestControllerIntegrationTest.java b/server/src/test/java/ru/practicum/shareit/request/ItemRequestControllerIntegrationTest.java new file mode 100644 index 0000000..9fe00e9 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/ItemRequestControllerIntegrationTest.java @@ -0,0 +1,122 @@ +package ru.practicum.shareit.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.Constants; +import ru.practicum.shareit.Random; +import ru.practicum.shareit.item.dao.ItemRepository; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.request.dao.ItemRequestRepository; +import ru.practicum.shareit.request.dto.ItemRequestCreate; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.dao.UserRepository; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.Collection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +public class ItemRequestControllerIntegrationTest { + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ItemRequestRepository requestRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ItemRepository itemRepository; + + @Test + public void shouldAddItemRequest() throws Exception { + User user = userRepository.save(Random.getUser()); + + ItemRequestCreate irc = Random.getItemRequest(); + + String json = objectMapper.writeValueAsString(irc); + + mockMvc.perform(post("/requests") + .header(Constants.USER_ID_HEADER, user.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").exists()) + .andExpect(jsonPath("$.description").value(irc.description())); + + Collection requests = requestRepository.findByRequestorIdOrderByCreatedDesc(user.getId()); + assertThat(requests).hasSize(1); + assertThat(requests.iterator().next().getDescription()).isEqualTo(irc.description()); + } + + @Test + public void shouldGetRequests() throws Exception { + User user = userRepository.save(Random.getUser()); + ItemRequest request = requestRepository.save(ItemRequest.builder() + .description("desc") + .requestorId(user.getId()) + .created(LocalDateTime.now()) + .build()); + + mockMvc.perform(get("/requests") + .header(Constants.USER_ID_HEADER, user.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(request.getId())) + .andExpect(jsonPath("$[0].description").value("desc")); + } + + @Test + public void shouldGetAllRequests() throws Exception { + User user = userRepository.save(Random.getUser()); + ItemRequest request = requestRepository.save(ItemRequest.builder() + .description("desc") + .requestorId(user.getId()) + .created(LocalDateTime.now()) + .build()); + + mockMvc.perform(get("/requests/all")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(request.getId())); + } + + @Test + public void shouldGetRequest() throws Exception { + User user = userRepository.save(Random.getUser()); + ItemRequest request = requestRepository.save(ItemRequest.builder() + .description("desc") + .requestorId(user.getId()) + .created(LocalDateTime.now()) + .build()); + Item item = itemRepository.save(Item.builder() + .name("item") + .description("desc") + .isAvailable(true) + .ownerId(user.getId()) + .requestId(request.getId()) + .build()); + + mockMvc.perform(get("/requests/{requestId}", request.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(request.getId())) + .andExpect(jsonPath("$.items[0].id").value(item.getId())); + } +} \ No newline at end of file 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..8cf0e19 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/ItemRequestControllerTest.java @@ -0,0 +1,88 @@ +package ru.practicum.shareit.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.Constants; +import ru.practicum.shareit.request.dto.ItemRequestCreate; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.service.ItemRequestService; + +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(ItemRequestController.class) +public class ItemRequestControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private ItemRequestService itemRequestService; + + @Test + public void shouldAddItemRequest() throws Exception { + ItemRequestCreate create = new ItemRequestCreate("description"); + ItemRequestDto dto = new ItemRequestDto(); + dto.setId(1L); + dto.setDescription("description"); + + when(itemRequestService.addItemRequest(any(), anyLong())).thenReturn(dto); + + mockMvc.perform(post("/requests") + .header(Constants.USER_ID_HEADER, 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(create))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.description").value("description")); + } + + @Test + public void shouldGetRequests() throws Exception { + ItemRequestDto dto = new ItemRequestDto(); + dto.setId(1L); + when(itemRequestService.getRequests(1L)).thenReturn(Collections.singletonList(dto)); + + mockMvc.perform(get("/requests") + .header(Constants.USER_ID_HEADER, 1L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)); + } + + @Test + public void shouldGetAllRequests() throws Exception { + ItemRequestDto dto = new ItemRequestDto(); + dto.setId(1L); + when(itemRequestService.getAllRequests()).thenReturn(Collections.singletonList(dto)); + + mockMvc.perform(get("/requests/all")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)); + } + + @Test + public void shouldGetRequest() throws Exception { + ItemRequestDto dto = new ItemRequestDto(); + dto.setId(1L); + when(itemRequestService.getRequest(1L)).thenReturn(dto); + + mockMvc.perform(get("/requests/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)); + } +} \ No newline at end of file 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..ad4dac7 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/ItemRequestMapperTest.java @@ -0,0 +1,41 @@ +package ru.practicum.shareit.request; + +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.request.dto.ItemRequestCreate; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.model.ItemRequest; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class ItemRequestMapperTest { + + @Test + public void shouldMapItemRequestCreateToItemRequest() { + ItemRequestCreate create = new ItemRequestCreate("Need a drill"); + Long requestorId = 5L; + + ItemRequest itemRequest = ItemRequestMapper.toItemRequest(create, requestorId); + + assertEquals(create.description(), itemRequest.getDescription()); + assertEquals(requestorId, itemRequest.getRequestorId()); + assertNotNull(itemRequest.getCreated()); + } + + @Test + public void shouldMapItemRequestToItemRequestDto() { + ItemRequest itemRequest = new ItemRequest(); + itemRequest.setId(10L); + itemRequest.setDescription("Need a saw"); + LocalDateTime created = LocalDateTime.of(2023, 5, 10, 12, 0); + itemRequest.setCreated(created); + + ItemRequestDto dto = ItemRequestMapper.toItemRequestDto(itemRequest); + + assertEquals(itemRequest.getId(), dto.getId()); + assertEquals(itemRequest.getDescription(), dto.getDescription()); + assertEquals(itemRequest.getCreated(), dto.getCreated()); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/request/dao/ItemRequestRepositoryTest.java b/server/src/test/java/ru/practicum/shareit/request/dao/ItemRequestRepositoryTest.java new file mode 100644 index 0000000..95d8eec --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/dao/ItemRequestRepositoryTest.java @@ -0,0 +1,44 @@ +package ru.practicum.shareit.request.dao; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import ru.practicum.shareit.Random; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.dao.UserRepository; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.Collection; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + + +@DataJpaTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class ItemRequestRepositoryTest { + + private final ItemRequestRepository itemRequestRepository; + + private final UserRepository userRepository; + + @Test + public void shouldFindByRequestorIdOrderByCreatedDesc() { + User user = userRepository.save(Random.getUser()); + ItemRequest request = new ItemRequest(); + request.setRequestorId(user.getId()); + request.setDescription(Random.getItemRequest().description()); + request.setCreated(LocalDateTime.now()); + + ItemRequest request1 = itemRequestRepository.save(request); + + Collection requests = itemRequestRepository.findByRequestorIdOrderByCreatedDesc(user.getId()); + + assertThat(requests).hasSize(1); + assertThat(requests.iterator().next().getId()).isEqualTo(request1.getId()); + } + +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceTest.java b/server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceTest.java new file mode 100644 index 0000000..bb16a74 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/service/ItemRequestServiceTest.java @@ -0,0 +1,99 @@ +package ru.practicum.shareit.request.service; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Sort; +import ru.practicum.shareit.item.dao.ItemRepository; +import ru.practicum.shareit.item.dto.ItemShortWithRequestId; +import ru.practicum.shareit.request.dao.ItemRequestRepository; +import ru.practicum.shareit.request.dto.ItemRequestCreate; +import ru.practicum.shareit.request.dto.ItemRequestDto; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.validation.exceptions.NotFoundException; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class ItemRequestServiceTest { + + @Mock + private ItemRequestRepository requestRepository; + + @Mock + private ItemRepository itemRepository; + + @InjectMocks + private ItemRequestService itemRequestService; + + @Test + public void shouldAddItemRequest() { + ItemRequestCreate create = new ItemRequestCreate("description"); + ItemRequest savedRequest = new ItemRequest(1L, "description", 1L, LocalDateTime.now()); + + when(requestRepository.save(any())).thenReturn(savedRequest); + + ItemRequestDto result = itemRequestService.addItemRequest(create, 1L); + + assertThat(result.getId()).isEqualTo(1L); + verify(requestRepository).save(any()); + } + + @Test + public void shouldGetRequests() { + ItemRequest request = new ItemRequest(1L, "desc", 1L, LocalDateTime.now()); + + when(requestRepository.findByRequestorIdOrderByCreatedDesc(1L)).thenReturn(List.of(request)); + when(itemRepository.findAllByRequestIdIn(any())).thenReturn(List.of()); + + Collection result = itemRequestService.getRequests(1L); + + assertThat(result).hasSize(1); + verify(requestRepository).findByRequestorIdOrderByCreatedDesc(1L); + } + + @Test + public void shouldGetAllRequests() { + ItemRequest request = new ItemRequest(1L, "desc", 1L, LocalDateTime.now()); + + when(requestRepository.findAll(any(Sort.class))).thenReturn(List.of(request)); + when(itemRepository.findAllByRequestIdIn(any())).thenReturn(List.of()); + + Collection result = itemRequestService.getAllRequests(); + + assertThat(result).hasSize(1); + verify(requestRepository).findAll(any(Sort.class)); + } + + @Test + public void shouldGetRequest() { + ItemRequest request = new ItemRequest(1L, "desc", 1L, LocalDateTime.now()); + ItemShortWithRequestId itemDto = mock(ItemShortWithRequestId.class); + + when(requestRepository.findById(1L)).thenReturn(Optional.of(request)); + when(itemRepository.findByRequestId(1L)).thenReturn(List.of(itemDto)); + + ItemRequestDto result = itemRequestService.getRequest(1L); + + assertThat(result.getId()).isEqualTo(1L); + verify(requestRepository).findById(1L); + } + + @Test + public void shouldThrowNotFoundExceptionWhenGetNonExistentRequest() { + when(requestRepository.findById(999L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> itemRequestService.getRequest(999L)) + .isInstanceOf(NotFoundException.class); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/UserControllerIntegrationTest.java b/server/src/test/java/ru/practicum/shareit/user/UserControllerIntegrationTest.java new file mode 100644 index 0000000..64fdf7f --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserControllerIntegrationTest.java @@ -0,0 +1,98 @@ +package ru.practicum.shareit.user; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.Random; +import ru.practicum.shareit.user.dao.UserRepository; +import ru.practicum.shareit.user.model.User; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +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; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +public class UserControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private UserRepository userRepository; + + @Test + public void shouldGetUser() throws Exception { + User user = userRepository.save(Random.getUser()); + + mockMvc.perform(get("/users/{userId}", user.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(user.getId())) + .andExpect(jsonPath("$.name").value(user.getName())); + } + + @Test + public void shouldAddUser() throws Exception { + User user = Random.getUser(); + String json = objectMapper.writeValueAsString(user); + + String responseContent = mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").exists()) + .andExpect(jsonPath("$.name").value(user.getName())) + .andReturn() + .getResponse() + .getContentAsString(); + + JsonNode jsonNode = objectMapper.readTree(responseContent); + Long returnedId = jsonNode.get("id").asLong(); + + Optional savedUser = userRepository.findById(returnedId); + assertThat(savedUser).isPresent(); + assertThat(savedUser.get().getName()).isEqualTo(user.getName()); + } + + @Test + public void shouldUpdateUser() throws Exception { + User user = userRepository.save(Random.getUser()); + User newUser = Random.getUser(); + String json = objectMapper.writeValueAsString(newUser); + + mockMvc.perform(patch("/users/{userId}", user.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value(newUser.getName())); + + Optional updatedUser = userRepository.findById(user.getId()); + assertThat(updatedUser).isPresent(); + assertThat(updatedUser.get().getName()).isEqualTo(newUser.getName()); + assertThat(updatedUser.get().getEmail()).isEqualTo(newUser.getEmail()); + } + + @Test + public void shouldDeleteUser() throws Exception { + User user = userRepository.save(Random.getUser()); + + mockMvc.perform(delete("/users/{userId}", user.getId())) + .andExpect(status().isOk()); + + Optional deletedUser = userRepository.findById(user.getId()); + assertThat(deletedUser).isEmpty(); + } +} \ No newline at end of file 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..10a0f1f --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserControllerTest.java @@ -0,0 +1,80 @@ +package ru.practicum.shareit.user; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.service.UserService; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(UserController.class) +public class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private UserService userService; + + @Test + public void shouldGetUser() throws Exception { + User user = new User(1L, "name", "email@test.com"); + when(userService.getUser(1L)).thenReturn(user); + + mockMvc.perform(get("/users/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("name")); + } + + @Test + public void shouldAddUser() throws Exception { + UserDto userDto = new UserDto(); + userDto.setName("name"); + userDto.setEmail("email@test.com"); + + User user = new User(1L, "name", "email@test.com"); + when(userService.addUser(any())).thenReturn(user); + + mockMvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)); + } + + @Test + public void shouldUpdateUser() throws Exception { + UserDto userDto = new UserDto(); + userDto.setName("new"); + userDto.setEmail("new@test.com"); + + User user = new User(1L, "new", "new@test.com"); + when(userService.updateUser(anyLong(), any())).thenReturn(user); + + mockMvc.perform(patch("/users/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("new")); + } + + @Test + public void shouldDeleteUser() throws Exception { + mockMvc.perform(delete("/users/1")) + .andExpect(status().isOk()); + } +} \ No newline at end of file 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..5c43565 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserMapperTest.java @@ -0,0 +1,68 @@ +package ru.practicum.shareit.user; + +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.model.User; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class UserMapperTest { + + @Test + public void shouldMapUserDtoToUser() { + UserDto userDto = new UserDto(); + userDto.setName("User Name"); + userDto.setEmail("user@example.com"); + + User user = UserMapper.toUser(userDto); + + assertEquals(userDto.getName(), user.getName()); + assertEquals(userDto.getEmail(), user.getEmail()); + } + + @Test + public void shouldMergeUserWhenAllFieldsNonNull() { + User user = new User(); + user.setName("Old Name"); + user.setEmail("old@example.com"); + + UserDto userDto = new UserDto(); + userDto.setName("New Name"); + userDto.setEmail("new@example.com"); + + User merged = UserMapper.merge(user, userDto); + + assertEquals(userDto.getName(), merged.getName()); + assertEquals(userDto.getEmail(), merged.getEmail()); + } + + @Test + public void shouldMergeUserWhenOnlyNameNotNull() { + User user = new User(); + user.setName("Old Name"); + user.setEmail("old@example.com"); + + UserDto userDto = new UserDto(); + userDto.setName("New Name"); + + User merged = UserMapper.merge(user, userDto); + + assertEquals(userDto.getName(), merged.getName()); + assertEquals("old@example.com", merged.getEmail()); + } + + @Test + public void shouldMergeUserWhenOnlyEmailNotNull() { + User user = new User(); + user.setName("Old Name"); + user.setEmail("old@example.com"); + + UserDto userDto = new UserDto(); + userDto.setEmail("new@example.com"); + + User merged = UserMapper.merge(user, userDto); + + assertEquals("Old Name", merged.getName()); + assertEquals(userDto.getEmail(), merged.getEmail()); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/service/UserServiceImplTest.java b/server/src/test/java/ru/practicum/shareit/user/service/UserServiceImplTest.java new file mode 100644 index 0000000..98a0f15 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/service/UserServiceImplTest.java @@ -0,0 +1,122 @@ +package ru.practicum.shareit.user.service; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import ru.practicum.shareit.user.dao.UserRepository; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.validation.exceptions.NotFoundException; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class UserServiceImplTest { + + @Mock + private UserRepository userRepository; + + @InjectMocks + private UserServiceImpl userService; + + @Test + public void shouldGetUser() { + User user = new User(1L, "name", "email@test.com"); + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + + User result = userService.getUser(1L); + + assertThat(result.getId()).isEqualTo(1L); + assertThat(result.getName()).isEqualTo("name"); + verify(userRepository).findById(1L); + } + + @Test + public void shouldThrowNotFoundExceptionWhenGetUserWithInvalidId() { + when(userRepository.findById(999L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.getUser(999L)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("Пользователь с id 999 не найден"); + } + + @Test + public void shouldAddUser() { + UserDto userDto = new UserDto(); + userDto.setName("name"); + userDto.setEmail("email@test.com"); + + User savedUser = new User(1L, "name", "email@test.com"); + when(userRepository.save(any())).thenReturn(savedUser); + + User result = userService.addUser(userDto); + + assertThat(result.getId()).isEqualTo(1L); + assertThat(result.getName()).isEqualTo("name"); + verify(userRepository).save(any()); + } + + @Test + public void shouldUpdateUser() { + User currentUser = new User(1L, "old", "old@test.com"); + UserDto updateDto = new UserDto(); + updateDto.setName("new"); + updateDto.setEmail("new@test.com"); + + when(userRepository.findById(1L)).thenReturn(Optional.of(currentUser)); + when(userRepository.save(any())).thenReturn(currentUser); + + User result = userService.updateUser(1L, updateDto); + + verify(userRepository).save(any()); + } + + @Test + public void shouldThrowNotFoundExceptionWhenUpdateNonExistentUser() { + UserDto updateDto = new UserDto(); + when(userRepository.findById(999L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> userService.updateUser(999L, updateDto)) + .isInstanceOf(NotFoundException.class); + } + + @Test + public void shouldDeleteUser() { + when(userRepository.existsById(1L)).thenReturn(true); + + userService.deleteUser(1L); + + verify(userRepository).deleteById(1L); + } + + @Test + public void shouldThrowNotFoundExceptionWhenDeleteNonExistentUser() { + when(userRepository.existsById(999L)).thenReturn(false); + + assertThatThrownBy(() -> userService.deleteUser(999L)) + .isInstanceOf(NotFoundException.class); + } + + @Test + public void shouldThrowIfUserNotFound() { + when(userRepository.existsById(999L)).thenReturn(false); + + assertThatThrownBy(() -> userService.throwIfUserNotFound(999L)) + .isInstanceOf(NotFoundException.class); + } + + @Test + public void shouldNotThrowWhenUserExists() { + when(userRepository.existsById(1L)).thenReturn(true); + + userService.throwIfUserNotFound(1L); + } +} \ No newline at end of file diff --git a/server/src/test/resources/application.properties b/server/src/test/resources/application.properties new file mode 100644 index 0000000..027e894 --- /dev/null +++ b/server/src/test/resources/application.properties @@ -0,0 +1,11 @@ +logging.level.ru.yandex.practicum.shareit=OFF +logging.level.org.springframework=WARN +spring.main.banner-mode=off + +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.username=dbuser +spring.datasource.password=12345 + +spring.sql.init.mode=always +spring.sql.init.schema-locations=classpath:schema.sql diff --git a/server/src/test/resources/schema.sql b/server/src/test/resources/schema.sql new file mode 100644 index 0000000..7b83546 --- /dev/null +++ b/server/src/test/resources/schema.sql @@ -0,0 +1,50 @@ +CREATE TABLE IF NOT EXISTS users ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(255) NOT NULL, + email VARCHAR(512) NOT NULL, + CONSTRAINT pk_user PRIMARY KEY (id), + CONSTRAINT UQ_USER_EMAIL UNIQUE (email) +); + +CREATE TABLE IF NOT EXISTS item_requests( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + description TEXT NOT NULL, + requestor_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT pk_item_request PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS items ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(255) NOT NULL, + description VARCHAR(255) NOT NULL, + available BOOLEAN NOT NULL, + owner_id BIGINT NOT NULL, + request_id BIGINT, + FOREIGN KEY (request_id) REFERENCES item_requests(id) ON DELETE SET NULL, + CONSTRAINT pk_item PRIMARY KEY (id), + FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS bookings ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + start_date TIMESTAMP WITHOUT TIME ZONE NOT NULL, + end_date TIMESTAMP WITHOUT TIME ZONE NOT NULL, + item_id BIGINT NOT NULL, + booker_id BIGINT NOT NULL, + status VARCHAR(255) NOT NULL, + CONSTRAINT pk_booking PRIMARY KEY (id), + FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE, + FOREIGN KEY (booker_id) REFERENCES users(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS comments ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + item_id BIGINT NOT NULL, + author_id BIGINT NOT NULL, + content TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT pk_comments PRIMARY KEY (id), + FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE, + FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE +); diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/ItemRequest.java deleted file mode 100644 index 95d6f23..0000000 --- a/src/main/java/ru/practicum/shareit/request/ItemRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.request; - -/** - * TODO Sprint add-item-requests. - */ -public class ItemRequest { -} diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequestController.java b/src/main/java/ru/practicum/shareit/request/ItemRequestController.java deleted file mode 100644 index 064e2e9..0000000 --- a/src/main/java/ru/practicum/shareit/request/ItemRequestController.java +++ /dev/null @@ -1,12 +0,0 @@ -package ru.practicum.shareit.request; - -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * TODO Sprint add-item-requests. - */ -@RestController -@RequestMapping(path = "/requests") -public class ItemRequestController { -} diff --git a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java b/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java deleted file mode 100644 index 7b3ed54..0000000 --- a/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.request.dto; - -/** - * TODO Sprint add-item-requests. - */ -public class ItemRequestDto { -} diff --git a/src/main/java/ru/practicum/shareit/validation/exceptions/NotUniqueException.java b/src/main/java/ru/practicum/shareit/validation/exceptions/NotUniqueException.java deleted file mode 100644 index ff5edd0..0000000 --- a/src/main/java/ru/practicum/shareit/validation/exceptions/NotUniqueException.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.validation.exceptions; - -public class NotUniqueException extends RuntimeException { - public NotUniqueException(String message) { - super(message); - } -}