diff --git a/compose.test.yml b/compose.test.yml new file mode 100644 index 0000000..6ad8203 --- /dev/null +++ b/compose.test.yml @@ -0,0 +1,90 @@ +services: + db: + image: postgres:16.1 + container_name: postgres + ports: + - "5445:5432" + volumes: + - ./volumes/shareit:/var/lib/postgresql/data/ + environment: + - SPRING_PROFILES_ACTIVE=dev + - POSTGRES_DB=shareit + - POSTGRES_USER=dbuser + - POSTGRES_PASSWORD=12345 + healthcheck: + test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER + timeout: 5s + interval: 5s + retries: 10 + + db-init: + image: postgres:16.1 + container_name: db-init-test + depends_on: + db: + condition: service_healthy + entrypoint: + - bash + - -c + - | + set -e + psql postgresql://dbuser:12345@db:5432/shareit -v ON_ERROR_STOP=1 <<-EOSQL + drop table IF EXISTS users CASCADE; + drop table IF EXISTS items CASCADE; + drop table IF EXISTS bookings CASCADE; + drop table IF EXISTS requests CASCADE; + drop table IF EXISTS comments CASCADE; + + 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 requests ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + description VARCHAR(2000), + requestor_id BIGINT NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + CONSTRAINT pk_request PRIMARY KEY (id), + CONSTRAINT fk_request_requestor FOREIGN KEY (requestor_id) REFERENCES users(id) + ); + + create TABLE IF NOT EXISTS items ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(255) NOT NULL, + description VARCHAR(2000), + available BOOLEAN DEFAULT TRUE, + owner_id BIGINT NOT NULL, + item_request_id BIGINT, + CONSTRAINT pk_item PRIMARY KEY (id), + CONSTRAINT fk_item_owner FOREIGN KEY (owner_id) REFERENCES users(id), + CONSTRAINT fk_item_item_request FOREIGN KEY (item_request_id) REFERENCES requests(id) + ); + + 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(50), + CONSTRAINT pk_booking PRIMARY KEY (id), + CONSTRAINT fk_booking_item FOREIGN KEY (item_id) REFERENCES items(id), + CONSTRAINT fk_booking_booker FOREIGN KEY (booker_id) REFERENCES users(id) + ); + + create TABLE IF NOT EXISTS comments ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + text VARCHAR(2000), + item_id BIGINT NOT NULL, + author_id BIGINT NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + CONSTRAINT pk_comment PRIMARY KEY (id), + CONSTRAINT fk_comment_item FOREIGN KEY (item_id) REFERENCES items(id), + CONSTRAINT fk_comment_author FOREIGN KEY (author_id) REFERENCES users(id) + ); + EOSQL + diff --git a/compose.yaml b/compose.yaml deleted file mode 100644 index 35ebf89..0000000 --- a/compose.yaml +++ /dev/null @@ -1,17 +0,0 @@ -services: - db: - image: postgres:16.1 - container_name: postgres5 - ports: - - "5438:5432" - volumes: - - ./volumes/postgres:/var/lib/postgresql/data/ - environment: - - POSTGRES_DB=shareitdb - - POSTGRES_USER=dbuser - - POSTGRES_PASSWORD=12345 - 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/compose.yml b/compose.yml new file mode 100644 index 0000000..ae83854 --- /dev/null +++ b/compose.yml @@ -0,0 +1,40 @@ +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_PROFILES_ACTIVE=dev + - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/shareit + - SPRING_DATASOURCE_USERNAME=dbuser + - SPRING_DATASOURCE_PASSWORD=12345 + + db: + image: postgres:16.1 + container_name: postgres + ports: + - "6541:5432" + environment: + - POSTGRES_PASSWORD=12345 + - POSTGRES_USER=dbuser + - POSTGRES_DB=shareit + healthcheck: + test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER + timeout: 5s + interval: 5s + retries: 10 \ No newline at end of file diff --git a/gateway/Dockerfile b/gateway/Dockerfile new file mode 100644 index 0000000..0ff1817 --- /dev/null +++ b/gateway/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:21-jre-jammy +VOLUME /tmp +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file diff --git a/gateway/pom.xml b/gateway/pom.xml new file mode 100644 index 0000000..f3394c1 --- /dev/null +++ b/gateway/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + ru.practicum + shareit + 0.0.1-SNAPSHOT + + + shareit-gateway + 0.0.1-SNAPSHOT + + ShareIt Gateway + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.hibernate.validator + hibernate-validator + + + + org.apache.httpcomponents.client5 + httpclient5 + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/shareit/ShareItGateway.java b/gateway/src/main/java/ru/practicum/shareit/ShareItGateway.java new file mode 100644 index 0000000..e659d11 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/ShareItGateway.java @@ -0,0 +1,12 @@ +package ru.practicum.shareit; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ShareItGateway { + public static void main(String[] args) { + SpringApplication.run(ShareItGateway.class, args); + } + +} diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/client/BookingClient.java b/gateway/src/main/java/ru/practicum/shareit/booking/client/BookingClient.java new file mode 100644 index 0000000..887e62e --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/client/BookingClient.java @@ -0,0 +1,55 @@ +package ru.practicum.shareit.booking.client; + +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.util.DefaultUriBuilderFactory; + +import ru.practicum.shareit.booking.dto.BookingRequestDto; +import ru.practicum.shareit.client.BaseClient; + +@Service +public class BookingClient extends BaseClient { + private static final String API_PREFIX = "/bookings"; + + @Autowired + public BookingClient(@Value("${shareit-server.url}") String serverUrl, RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + API_PREFIX)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + } + + public ResponseEntity createBooking(long userId, BookingRequestDto bookingRequestDto) { + return post("", userId, bookingRequestDto); + } + + public ResponseEntity updateBooking(Long userId, Long bookingId, Boolean approved) { + Map parameters = Map.of("approved", approved); + + return patch("/" + bookingId + "?approved={approved}", userId, parameters, null); + } + + public ResponseEntity findBookingById(long userId, Long bookingId) { + return get("/" + bookingId, userId); + } + + public ResponseEntity findUserBookings(Long userId, String state) { + Map parameters = Map.of("state", state); + + return get("?state={state}", userId, parameters); + } + + public ResponseEntity findOwnerReservedItems(Long userId, String state) { + Map parameters = Map.of("state", state); + + return get("/owner?state={state}", userId, parameters); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java b/gateway/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java new file mode 100644 index 0000000..b59123a --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java @@ -0,0 +1,54 @@ +package ru.practicum.shareit.booking.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import ru.practicum.shareit.booking.client.BookingClient; +import ru.practicum.shareit.booking.dto.BookingRequestDto; + + +@Controller +@RequestMapping(path = "/bookings") +@RequiredArgsConstructor +@Slf4j +@Validated +public class BookingController { + private final BookingClient bookingClient; + + @PostMapping + public ResponseEntity createBooking(@RequestHeader("X-Sharer-User-Id") long userId, + @RequestBody @Valid BookingRequestDto bookingRequestDto) { + log.info("Создание бронирования {}, userId={}", bookingRequestDto, userId); + return bookingClient.createBooking(userId, bookingRequestDto); + } + + @PatchMapping("/{bookingId}") + public ResponseEntity updateBooking(@RequestHeader("X-Sharer-User-Id") Long userId, @PathVariable("bookingId") Long bookingId, @RequestParam(name = "approved") Boolean approved) { + log.info("Идентификатор бронирования {} по идентификатору пользователя {}", bookingId, userId); + return bookingClient.updateBooking(userId, bookingId, approved); + } + + @GetMapping("/{bookingId}") + public ResponseEntity findBookingById(@RequestHeader("X-Sharer-User-Id") long userId, + @PathVariable Long bookingId) { + log.info("Забронировать {}, userId={}", bookingId, userId); + return bookingClient.findBookingById(userId, bookingId); + } + + @GetMapping + public ResponseEntity findUserBookings(@RequestHeader("X-Sharer-User-Id") Long userId, @RequestParam(name = "state", defaultValue = "ALL") String state) { + log.info("Получить бронирования {}.", state); + return bookingClient.findUserBookings(userId, state); + } + + @GetMapping("/owner") + public ResponseEntity findOwnerReservedItems(@RequestHeader("X-Sharer-User-Id") Long userId, @RequestParam(name = "state", defaultValue = "ALL") String state) { + log.info("Получить зарезервированные товары владельца{}.", state); + return bookingClient.findOwnerReservedItems(userId, state); + } +} diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingRequestDto.java b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingRequestDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/dto/BookingRequestDto.java rename to gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingRequestDto.java diff --git a/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java similarity index 78% rename from src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java rename to gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java index 334abb1..67e3ef1 100644 --- a/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java +++ b/gateway/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java @@ -1,6 +1,5 @@ package ru.practicum.shareit.booking.dto; -import com.fasterxml.jackson.annotation.JsonFormat; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; @@ -15,9 +14,7 @@ @Setter public class BookingResponseDto { private Long id; - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") private LocalDateTime start; - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") private LocalDateTime end; private ItemDto item; private UserDto booker; diff --git a/src/main/java/ru/practicum/shareit/booking/status/RentalStatus.java b/gateway/src/main/java/ru/practicum/shareit/booking/status/RentalStatus.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/status/RentalStatus.java rename to gateway/src/main/java/ru/practicum/shareit/booking/status/RentalStatus.java 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..1a2d33a --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/client/BaseClient.java @@ -0,0 +1,121 @@ +package ru.practicum.shareit.client; + +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.Nullable; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; + +public class BaseClient { + protected final RestTemplate rest; + + public BaseClient(RestTemplate rest) { + this.rest = rest; + } + + protected ResponseEntity get(String path) { + return get(path, null, null); + } + + protected ResponseEntity get(String path, long userId) { + return get(path, userId, null); + } + + protected ResponseEntity get(String path, Long userId, @Nullable Map parameters) { + return makeAndSendRequest(HttpMethod.GET, path, userId, parameters, null); + } + + protected ResponseEntity post(String path, T body) { + return post(path, null, null, body); + } + + protected ResponseEntity post(String path, long userId, T body) { + return post(path, userId, null, body); + } + + protected ResponseEntity post(String path, Long userId, @Nullable Map parameters, T body) { + return makeAndSendRequest(HttpMethod.POST, path, userId, parameters, body); + } + + protected ResponseEntity put(String path, long userId, T body) { + return put(path, userId, null, body); + } + + protected ResponseEntity put(String path, long userId, @Nullable Map parameters, T body) { + return makeAndSendRequest(HttpMethod.PUT, path, userId, parameters, body); + } + + protected ResponseEntity patch(String path, T body) { + return patch(path, null, null, body); + } + + protected ResponseEntity patch(String path, long userId) { + return patch(path, userId, null, null); + } + + protected ResponseEntity patch(String path, long userId, T body) { + return patch(path, userId, null, body); + } + + protected ResponseEntity patch(String path, Long userId, @Nullable Map parameters, T body) { + return makeAndSendRequest(HttpMethod.PATCH, path, userId, parameters, body); + } + + protected ResponseEntity delete(String path) { + return delete(path, null, null); + } + + protected ResponseEntity delete(String path, long userId) { + return delete(path, userId, null); + } + + protected ResponseEntity delete(String path, Long userId, @Nullable Map parameters) { + return makeAndSendRequest(HttpMethod.DELETE, path, userId, parameters, null); + } + + private ResponseEntity makeAndSendRequest(HttpMethod method, String path, Long userId, @Nullable Map parameters, @Nullable T body) { + HttpEntity requestEntity = new HttpEntity<>(body, defaultHeaders(userId)); + + ResponseEntity shareitServerResponse; + try { + if (parameters != null) { + shareitServerResponse = rest.exchange(path, method, requestEntity, Object.class, parameters); + } else { + shareitServerResponse = rest.exchange(path, method, requestEntity, Object.class); + } + } catch (HttpStatusCodeException e) { + return ResponseEntity.status(e.getStatusCode()).body(e.getResponseBodyAsByteArray()); + } + 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(); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/item/client/ItemClient.java b/gateway/src/main/java/ru/practicum/shareit/item/client/ItemClient.java new file mode 100644 index 0000000..18d8c45 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/client/ItemClient.java @@ -0,0 +1,55 @@ +package ru.practicum.shareit.item.client; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.util.DefaultUriBuilderFactory; +import ru.practicum.shareit.client.BaseClient; +import ru.practicum.shareit.item.comment.dto.CommentRequestDto; +import ru.practicum.shareit.item.dto.ItemDto; + +import java.util.Map; + +@Service +public class ItemClient extends BaseClient { + private static final String API_PREFIX = "/items"; + + @Autowired + public ItemClient(@Value("${shareit-server.url}") String serverUrl, RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + API_PREFIX)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + } + + public ResponseEntity createItem(Long userId, ItemDto itemDto) { + return (post("", userId, itemDto)); + } + + public ResponseEntity updateItem(Long userId, Long itemId, ItemDto itemDto) { + return (patch("/" + itemId, userId, itemDto)); + } + + public ResponseEntity findItemById(Long userId, Long itemId) { + return get("/" + itemId, userId); + } + + public ResponseEntity findUserItems(Long userId) { + return get("", userId); + } + + public ResponseEntity searchItem(Long userId, String text) { + Map parameters = Map.of("text", text); + + return get("/search?text={text}", userId, parameters); + } + + public ResponseEntity createComment(Long userId, Long itemId, CommentRequestDto commentRequestDto) { + return post("/" + itemId + "/comment", userId, commentRequestDto); + } +} diff --git a/src/main/java/ru/practicum/shareit/item/comment/dto/CommentRequestDto.java b/gateway/src/main/java/ru/practicum/shareit/item/comment/dto/CommentRequestDto.java similarity index 80% rename from src/main/java/ru/practicum/shareit/item/comment/dto/CommentRequestDto.java rename to gateway/src/main/java/ru/practicum/shareit/item/comment/dto/CommentRequestDto.java index 7943bae..e4efc14 100644 --- a/src/main/java/ru/practicum/shareit/item/comment/dto/CommentRequestDto.java +++ b/gateway/src/main/java/ru/practicum/shareit/item/comment/dto/CommentRequestDto.java @@ -2,7 +2,10 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; @NoArgsConstructor @AllArgsConstructor diff --git a/src/main/java/ru/practicum/shareit/item/comment/dto/CommentResponseDto.java b/gateway/src/main/java/ru/practicum/shareit/item/comment/dto/CommentResponseDto.java similarity index 78% rename from src/main/java/ru/practicum/shareit/item/comment/dto/CommentResponseDto.java rename to gateway/src/main/java/ru/practicum/shareit/item/comment/dto/CommentResponseDto.java index 5708e07..798ec01 100644 --- a/src/main/java/ru/practicum/shareit/item/comment/dto/CommentResponseDto.java +++ b/gateway/src/main/java/ru/practicum/shareit/item/comment/dto/CommentResponseDto.java @@ -1,6 +1,5 @@ package ru.practicum.shareit.item.comment.dto; -import com.fasterxml.jackson.annotation.JsonFormat; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; @@ -14,7 +13,6 @@ public class CommentResponseDto { private Long id; private String text; private String authorName; - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") private LocalDateTime created; private Long itemId; } diff --git a/gateway/src/main/java/ru/practicum/shareit/item/controller/ItemController.java b/gateway/src/main/java/ru/practicum/shareit/item/controller/ItemController.java new file mode 100644 index 0000000..cc62894 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/item/controller/ItemController.java @@ -0,0 +1,57 @@ +package ru.practicum.shareit.item.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.item.client.ItemClient; +import ru.practicum.shareit.item.comment.dto.CommentRequestDto; +import ru.practicum.shareit.item.dto.ItemDto; + + +@RequiredArgsConstructor +@Slf4j +@RestController +@RequestMapping("/items") +public class ItemController { + private final ItemClient itemClient; + + @PostMapping + public ResponseEntity createItem(@RequestHeader("X-Sharer-User-Id") Long userId, @Valid @RequestBody ItemDto itemDto) { + log.info("POST /items"); + return itemClient.createItem(userId, itemDto); + } + + @PatchMapping("{itemId}") + public ResponseEntity updateItem(@RequestHeader("X-Sharer-User-Id") Long userId, @PathVariable("itemId") Long itemId, @RequestBody ItemDto itemDto) { + log.info("PATCH /items/{}", itemId); + return itemClient.updateItem(itemId, userId, itemDto); + } + + @GetMapping("/{itemId}") + public ResponseEntity findItemById(@RequestHeader("X-Sharer-User-Id") Long userId, @PathVariable("itemId") Long itemId) { + log.info("GET /items/{}", itemId); + return itemClient.findItemById(userId, itemId); + } + + @GetMapping + public ResponseEntity findUserItems(@RequestHeader("X-Sharer-User-Id") Long userId) { + log.info("GET /items"); + return itemClient.findUserItems(userId); + } + + @GetMapping("/search") + public ResponseEntity searchItem(@RequestHeader("X-Sharer-User-Id") Long userId, @RequestParam(name = "text") String text) { + log.info("GET /items/search?text={}.", text); + return itemClient.searchItem(userId, text); + } + + @PostMapping("/{itemId}/comment") + public ResponseEntity createComment(@RequestHeader("X-Sharer-User-Id") Long userId, + @PathVariable("itemId") Long itemId, + @Valid @RequestBody CommentRequestDto commentRequestDto) { + log.info("POST /items/{}/comment", itemId); + return itemClient.createComment(userId, itemId, commentRequestDto); + } +} diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java b/gateway/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java similarity index 95% rename from src/main/java/ru/practicum/shareit/item/dto/ItemDto.java rename to gateway/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java index a750b8a..ca6eed4 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ b/gateway/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -17,4 +17,5 @@ public class ItemDto { private String description; @NotNull(message = "Поле доступ не может быть null") private Boolean available; + private Long requestId; } diff --git a/gateway/src/main/java/ru/practicum/shareit/request/client/ItemRequestClient.java b/gateway/src/main/java/ru/practicum/shareit/request/client/ItemRequestClient.java new file mode 100644 index 0000000..bfcfe9f --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/request/client/ItemRequestClient.java @@ -0,0 +1,47 @@ +package ru.practicum.shareit.request.client; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.util.DefaultUriBuilderFactory; +import ru.practicum.shareit.client.BaseClient; +import ru.practicum.shareit.request.dto.ItemRequestDto; + +import java.util.Map; + +@Service +public class ItemRequestClient extends BaseClient { + private static final String API_PREFIX = "/requests"; + + @Autowired + public ItemRequestClient(@Value("${shareit-server.url}") String serverUrl, RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + API_PREFIX)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + } + + public ResponseEntity createItemRequest(Long userId, ItemRequestDto itemRequestDto) { + return post("", userId, itemRequestDto); + } + + public ResponseEntity findListOfYourQueriesWithAnswers(@RequestHeader("X-Sharer-User-Id") Long userId) { + return get("", userId); + } + + public ResponseEntity findListOfRequestsOtherUsers(Long userId, int from, int size) { + Map parameters = Map.of("from", from, "size", size); + + return get("/all?from={from}&size={size}", userId, parameters); + } + + public ResponseEntity findYourQueryWithAnswers(Long userId, Long requestId) { + return get("/" + requestId, userId); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/request/controller/ItemRequestController.java b/gateway/src/main/java/ru/practicum/shareit/request/controller/ItemRequestController.java new file mode 100644 index 0000000..87d1351 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/request/controller/ItemRequestController.java @@ -0,0 +1,42 @@ +package ru.practicum.shareit.request.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.request.client.ItemRequestClient; +import ru.practicum.shareit.request.dto.ItemRequestDto; + +@RequiredArgsConstructor +@Slf4j +@RestController +@RequestMapping(path = "/requests") +public class ItemRequestController { + private final ItemRequestClient itemRequestClient; + + @PostMapping + public ResponseEntity createItemRequest(@RequestHeader("X-Sharer-User-Id") Long userId, @RequestBody ItemRequestDto itemRequestDto) { + log.info("POST /requests"); + return itemRequestClient.createItemRequest(userId, itemRequestDto); + } + + @GetMapping + public ResponseEntity findListOfYourQueriesWithAnswers(@RequestHeader("X-Sharer-User-Id") Long userId) { + log.info("GET /requests"); + return itemRequestClient.findListOfYourQueriesWithAnswers(userId); + } + + @GetMapping("/all") + public ResponseEntity findListOfRequestsOtherUsers(@RequestHeader("X-Sharer-User-Id") Long userId, + @RequestParam(defaultValue = "0") int from, + @RequestParam(defaultValue = "10") int size) { + log.info("GET /requests/all"); + return itemRequestClient.findListOfRequestsOtherUsers(userId, from, size); + } + + @GetMapping("/{requestId}") + public ResponseEntity findYourQueryWithAnswers(@RequestHeader("X-Sharer-User-Id") Long userId, @PathVariable Long requestId) { + log.info("GET /requests/{}", requestId); + return itemRequestClient.findYourQueryWithAnswers(userId, requestId); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java b/gateway/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java new file mode 100644 index 0000000..92c4972 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/request/dto/ItemRequestDto.java @@ -0,0 +1,21 @@ +package ru.practicum.shareit.request.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Setter +@Getter +public class ItemRequestDto { + @Size(max = 2000, message = "Описание не может быть длиннее 2000 символов") + @NotBlank(message = "Описание не может быть пустым") + private String description; + @NotNull(message = "UserId не может быть null") + private Long requestorId; +} diff --git a/gateway/src/main/java/ru/practicum/shareit/request/dto/ItemResponseDto.java b/gateway/src/main/java/ru/practicum/shareit/request/dto/ItemResponseDto.java new file mode 100644 index 0000000..e372466 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/request/dto/ItemResponseDto.java @@ -0,0 +1,22 @@ +package ru.practicum.shareit.request.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.shareit.item.dto.ItemDto; + +import java.time.LocalDateTime; +import java.util.List; + +@NoArgsConstructor +@AllArgsConstructor +@Setter +@Getter +public class ItemResponseDto { + private Long id; + private String description; + private Long userId; + private LocalDateTime created; + private List items; +} diff --git a/gateway/src/main/java/ru/practicum/shareit/user/client/UserClient.java b/gateway/src/main/java/ru/practicum/shareit/user/client/UserClient.java new file mode 100644 index 0000000..f87fa26 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/user/client/UserClient.java @@ -0,0 +1,46 @@ +package ru.practicum.shareit.user.client; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.util.DefaultUriBuilderFactory; +import ru.practicum.shareit.client.BaseClient; +import ru.practicum.shareit.user.dto.UserDto; + +@Service +public class UserClient extends BaseClient { + private static final String API_PREFIX = "/users"; + + @Autowired + public UserClient(@Value("${shareit-server.url}") String serverUrl, RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + API_PREFIX)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + } + + public ResponseEntity findAllUsers() { + return get(""); + } + + public ResponseEntity findUserById(Long id) { + return get("/" + id); + } + + public ResponseEntity createUser(UserDto userDto) { + return post("", userDto); + } + + public ResponseEntity updateUser(Long id, UserDto userDto) { + return patch("/" + id, userDto); + } + + public ResponseEntity deleteUser(Long id) { + return delete("/" + id); + } +} diff --git a/gateway/src/main/java/ru/practicum/shareit/user/controller/UserController.java b/gateway/src/main/java/ru/practicum/shareit/user/controller/UserController.java new file mode 100644 index 0000000..ace155b --- /dev/null +++ b/gateway/src/main/java/ru/practicum/shareit/user/controller/UserController.java @@ -0,0 +1,51 @@ +package ru.practicum.shareit.user.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.client.UserClient; + + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/users") +@Validated +public class UserController { + + private final UserClient userClient; + + @GetMapping + public ResponseEntity findAllUsers() { + log.info("GET /users"); + return userClient.findAllUsers(); + } + + @GetMapping("/{id}") + public ResponseEntity findUserById(@PathVariable("id") Long id) { + log.info("GET /users/{}", id); + return userClient.findUserById(id); + } + + @PostMapping + public ResponseEntity createUser(@Valid @RequestBody UserDto userDto) { + log.info("POST /users"); + return userClient.createUser(userDto); + } + + @PatchMapping("/{id}") + public ResponseEntity updateUser(@PathVariable("id") Long id, @RequestBody UserDto userDto) { + log.info("PATCH /users/{}", id); + return userClient.updateUser(id, userDto); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteUser(@PathVariable Long id) { + log.info("DELETE /users/{}", id); + return userClient.deleteUser(id); + } +} diff --git a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java b/gateway/src/main/java/ru/practicum/shareit/user/dto/UserDto.java similarity index 82% rename from src/main/java/ru/practicum/shareit/user/dto/UserDto.java rename to gateway/src/main/java/ru/practicum/shareit/user/dto/UserDto.java index bf78cf7..a7549a5 100644 --- a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java +++ b/gateway/src/main/java/ru/practicum/shareit/user/dto/UserDto.java @@ -2,7 +2,10 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; @NoArgsConstructor @AllArgsConstructor diff --git a/gateway/src/main/resources/application.yml b/gateway/src/main/resources/application.yml new file mode 100644 index 0000000..8fce774 --- /dev/null +++ b/gateway/src/main/resources/application.yml @@ -0,0 +1,13 @@ +logging: + level: + org: + springframework: + web: + client: + RestTemplate: DEBUG + +server: + port: 8080 + +shareit-server: + url: http://localhost:9090 \ No newline at end of file diff --git a/gateway/src/test/java/ru/practicum/shareit/booking/BookingRequestDtoValidationTest.java b/gateway/src/test/java/ru/practicum/shareit/booking/BookingRequestDtoValidationTest.java new file mode 100644 index 0000000..40be203 --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/booking/BookingRequestDtoValidationTest.java @@ -0,0 +1,85 @@ +package ru.practicum.shareit.booking; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.booking.dto.BookingRequestDto; + +import java.time.LocalDateTime; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BookingRequestDtoValidationTest { + private static Validator validator; + + @BeforeAll + static void setupValidator() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + void mustFailWhenItemIdIsNull() { + BookingRequestDto dto = new BookingRequestDto( + null, + LocalDateTime.now().plusHours(2), + LocalDateTime.now().plusDays(1) + ); + + Set> violations = validator.validate(dto); + assertThat(violations).anyMatch(v -> v.getPropertyPath().toString().equals("itemId")); + } + + @Test + void mustFailWhenStartIsInPast() { + BookingRequestDto dto = new BookingRequestDto( + 1L, + LocalDateTime.now().minusDays(1), + LocalDateTime.now().plusDays(1) + ); + + Set> violations = validator.validate(dto); + assertThat(violations).anyMatch(v -> v.getPropertyPath().toString().equals("start")); + } + + @Test + void mustFailWhenEndIsInPast() { + BookingRequestDto dto = new BookingRequestDto( + 1L, + LocalDateTime.now().plusHours(1), + LocalDateTime.now().minusHours(1) + ); + + Set> violations = validator.validate(dto); + assertThat(violations).anyMatch(v -> v.getPropertyPath().toString().equals("end")); + } + + @Test + void mustFailWhenStartOrEndIsNull() { + BookingRequestDto dto = new BookingRequestDto( + 1L, + null, + null + ); + + Set> violations = validator.validate(dto); + assertThat(violations).anyMatch(v -> v.getPropertyPath().toString().equals("start")); + assertThat(violations).anyMatch(v -> v.getPropertyPath().toString().equals("end")); + } + + @Test + void mustPassValidation() { + BookingRequestDto dto = new BookingRequestDto( + 1L, + LocalDateTime.now().plusHours(2), + LocalDateTime.now().plusDays(1) + ); + + Set> violations = validator.validate(dto); + assertThat(violations).isEmpty(); + } +} diff --git a/gateway/src/test/java/ru/practicum/shareit/item/ItemDtoValidationTest.java b/gateway/src/test/java/ru/practicum/shareit/item/ItemDtoValidationTest.java new file mode 100644 index 0000000..91aa286 --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/item/ItemDtoValidationTest.java @@ -0,0 +1,62 @@ +package ru.practicum.shareit.item; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.item.dto.ItemDto; + +import java.util.Set; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +public class ItemDtoValidationTest { + private static Validator validator; + + @BeforeAll + static void setupValidator() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + void mustFailValidationWhenNameIsBlank() { + ItemDto dto = new ItemDto(1L, " ", "Полупрофессиональная модель фотоаппарата", true, null); + + Set> violations = validator.validate(dto); + + assertThat(violations) + .anyMatch(v -> v.getPropertyPath().toString().equals("name")); + } + + @Test + void mustFailValidationWhenDescriptionIsBlank() { + ItemDto dto = new ItemDto(1L, "Canon 500d", " ", true, null); + + Set> violations = validator.validate(dto); + + assertThat(violations) + .anyMatch(v -> v.getPropertyPath().toString().equals("description")); + } + + @Test + void mustFailValidationWhenAvailableIsNull() { + ItemDto dto = new ItemDto(1L, "Canon 500d", "Полупрофессиональная модель фотоаппарата", null, null); + + Set> violations = validator.validate(dto); + + assertThat(violations) + .anyMatch(v -> v.getPropertyPath().toString().equals("available")); + } + + @Test + void mustPassValidationForValidDto() { + ItemDto dto = new ItemDto(1L, "Canon 500d", "Полупрофессиональная модель фотоаппарата", true, null); + + Set> violations = validator.validate(dto); + + assertThat(violations).isEmpty(); + } +} diff --git a/gateway/src/test/java/ru/practicum/shareit/item/comment/CommentRequestDtoValidationTest.java b/gateway/src/test/java/ru/practicum/shareit/item/comment/CommentRequestDtoValidationTest.java new file mode 100644 index 0000000..81ce72c --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/item/comment/CommentRequestDtoValidationTest.java @@ -0,0 +1,53 @@ +package ru.practicum.shareit.item.comment; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.item.comment.dto.CommentRequestDto; + +import java.util.Set; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +public class CommentRequestDtoValidationTest { + private static Validator validator; + + @BeforeAll + static void setupValidator() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + void mustFailValidationWhenTextIsBlank() { + CommentRequestDto dto = new CommentRequestDto(" "); + + Set> violations = validator.validate(dto); + + assertThat(violations) + .anyMatch(v -> v.getPropertyPath().toString().equals("text")); + } + + @Test + void mustFailValidationWhenTextIsTooLong() { + String longText = "Тест".repeat(2001); + CommentRequestDto dto = new CommentRequestDto(longText); + + Set> violations = validator.validate(dto); + + assertThat(violations) + .anyMatch(v -> v.getPropertyPath().toString().equals("text")); + } + + @Test + void mustPassValidationWhenTextIsValid() { + CommentRequestDto dto = new CommentRequestDto("Хорошая вещь. Все отлично!"); + + Set> violations = validator.validate(dto); + + assertThat(violations).isEmpty(); + } +} diff --git a/gateway/src/test/java/ru/practicum/shareit/request/ItemReqRequestDtoValidationTest.java b/gateway/src/test/java/ru/practicum/shareit/request/ItemReqRequestDtoValidationTest.java new file mode 100644 index 0000000..5938d1f --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/request/ItemReqRequestDtoValidationTest.java @@ -0,0 +1,63 @@ +package ru.practicum.shareit.request; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.request.dto.ItemRequestDto; + +import java.util.Set; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +public class ItemReqRequestDtoValidationTest { + private static Validator validator; + + @BeforeAll + static void setupValidator() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + void mustFailValidationWhenDescriptionIsBlank() { + ItemRequestDto dto = new ItemRequestDto(" ", 1L); + + Set> violations = validator.validate(dto); + + assertThat(violations) + .anyMatch(v -> v.getPropertyPath().toString().equals("description")); + } + + @Test + void mustFailValidationWhenDescriptionTooLong() { + String longDescription = "test".repeat(2001); + ItemRequestDto dto = new ItemRequestDto(longDescription, 1L); + + Set> violations = validator.validate(dto); + + assertThat(violations) + .anyMatch(v -> v.getPropertyPath().toString().equals("description")); + } + + @Test + void mustFailValidationWhenRequestorIdIsNull() { + ItemRequestDto dto = new ItemRequestDto("Описание для теста", null); + + Set> violations = validator.validate(dto); + + assertThat(violations) + .anyMatch(v -> v.getPropertyPath().toString().equals("requestorId")); + } + + @Test + void mustPassValidationForValidDto() { + ItemRequestDto dto = new ItemRequestDto("Описание для теста", 1L); + + Set> violations = validator.validate(dto); + + assertThat(violations).isEmpty(); + } +} diff --git a/gateway/src/test/java/ru/practicum/shareit/user/UserDtoValidationTest.java b/gateway/src/test/java/ru/practicum/shareit/user/UserDtoValidationTest.java new file mode 100644 index 0000000..37fec2c --- /dev/null +++ b/gateway/src/test/java/ru/practicum/shareit/user/UserDtoValidationTest.java @@ -0,0 +1,59 @@ +package ru.practicum.shareit.user; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.user.dto.UserDto; + +import java.util.Set; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +public class UserDtoValidationTest { + private static Validator validator; + + @BeforeAll + static void setupValidator() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + void mustFailValidationWhenNameIsBlank() { + UserDto userDto = new UserDto(1L, "", "Max@mail.ru"); + + Set> violations = validator.validate(userDto); + + assertThat(violations).anyMatch(v -> v.getPropertyPath().toString().equals("name")); + } + + @Test + void mustFailValidationWhenEmailIsInvalid() { + UserDto userDto = new UserDto(1L, "Макс Иванов", "Maxmail.ru"); + + Set> violations = validator.validate(userDto); + + assertThat(violations).anyMatch(v -> v.getPropertyPath().toString().equals("email")); + } + + @Test + void mustFailValidationWhenEmailIsBlank() { + UserDto userDto = new UserDto(1L, "Макс Иванов", ""); + + Set> violations = validator.validate(userDto); + + assertThat(violations).anyMatch(v -> v.getPropertyPath().toString().equals("email")); + } + + @Test + void mustPassValidationWhenAllFieldsAreValid() { + UserDto userDto = new UserDto(1L, "Макс Иванов", "Max@mail.ru"); + + Set> violations = validator.validate(userDto); + + assertThat(violations).isEmpty(); + } +} diff --git a/pom.xml b/pom.xml index b1f2886..ba25020 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,7 @@ ru.practicum shareit + pom 0.0.1-SNAPSHOT ShareIt @@ -19,6 +20,11 @@ 21 + + gateway + server + + org.springframework.boot @@ -26,31 +32,28 @@ org.springframework.boot - spring-boot-starter-actuator + spring-boot-starter-data-jpa + + + org.postgresql + postgresql + runtime + + + com.h2database + h2 + runtime org.springframework.boot spring-boot-configuration-processor true - org.projectlombok lombok true - - - com.h2database - h2 - runtime - - - - org.postgresql - postgresql - runtime - org.springframework.boot spring-boot-starter-test @@ -61,34 +64,30 @@ spring-boot-starter-validation - org.springframework.boot - spring-boot-starter-data-jpa + org.apache.commons + commons-lang3 + 3.12.0 - - - src/main/resources - true - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - + + org.springframework.boot + spring-boot-maven-plugin + + + true + + + + org.projectlombok + lombok + + + + org.apache.maven.plugins maven-surefire-plugin @@ -235,17 +234,5 @@ - - coverage - - - - org.jacoco - jacoco-maven-plugin - - - - - \ No newline at end of file diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..0ff1817 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:21-jre-jammy +VOLUME /tmp +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file diff --git a/server/pom.xml b/server/pom.xml new file mode 100644 index 0000000..c561c04 --- /dev/null +++ b/server/pom.xml @@ -0,0 +1,92 @@ + + + 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.hibernate.validator + hibernate-validator + 8.0.2.Final + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + coverage + + + + org.jacoco + jacoco-maven-plugin + + + + + + + diff --git a/src/main/java/ru/practicum/shareit/ShareItApp.java b/server/src/main/java/ru/practicum/shareit/ShareItApp.java similarity index 65% rename from src/main/java/ru/practicum/shareit/ShareItApp.java rename to server/src/main/java/ru/practicum/shareit/ShareItApp.java index a00ad56..a10a87d 100644 --- a/src/main/java/ru/practicum/shareit/ShareItApp.java +++ b/server/src/main/java/ru/practicum/shareit/ShareItApp.java @@ -6,8 +6,8 @@ @SpringBootApplication public class ShareItApp { - public static void main(String[] args) { - SpringApplication.run(ShareItApp.class, args); - } + public static void main(String[] args) { + SpringApplication.run(ShareItApp.class, args); + } } diff --git a/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java b/server/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java similarity index 94% rename from src/main/java/ru/practicum/shareit/booking/controller/BookingController.java rename to server/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java index a6a71d7..d692dfa 100644 --- a/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java +++ b/server/src/main/java/ru/practicum/shareit/booking/controller/BookingController.java @@ -1,6 +1,5 @@ package ru.practicum.shareit.booking.controller; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; @@ -18,7 +17,7 @@ public class BookingController { private final BookingService bookingService; @PostMapping - public BookingResponseDto createBooking(@RequestHeader("X-Sharer-User-Id") Long userId, @Valid @RequestBody BookingRequestDto bookingRequestDto) { + public BookingResponseDto createBooking(@RequestHeader("X-Sharer-User-Id") Long userId, @RequestBody BookingRequestDto bookingRequestDto) { log.info("POST /bookings"); return bookingService.createBooking(userId, bookingRequestDto); } diff --git a/server/src/main/java/ru/practicum/shareit/booking/dto/BookingRequestDto.java b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingRequestDto.java new file mode 100644 index 0000000..933e991 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingRequestDto.java @@ -0,0 +1,16 @@ +package ru.practicum.shareit.booking.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@AllArgsConstructor +@Getter +@Setter +public class BookingRequestDto { + private Long itemId; + private LocalDateTime start; + private LocalDateTime end; +} diff --git a/server/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java new file mode 100644 index 0000000..67e3ef1 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/dto/BookingResponseDto.java @@ -0,0 +1,22 @@ +package ru.practicum.shareit.booking.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import ru.practicum.shareit.booking.status.RentalStatus; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.user.dto.UserDto; + +import java.time.LocalDateTime; + +@AllArgsConstructor +@Getter +@Setter +public class BookingResponseDto { + private Long id; + private LocalDateTime start; + private LocalDateTime end; + private ItemDto item; + private UserDto booker; + private RentalStatus status; +} diff --git a/src/main/java/ru/practicum/shareit/booking/dto/SimplifiedBookingDto.java b/server/src/main/java/ru/practicum/shareit/booking/dto/SimplifiedBookingDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/dto/SimplifiedBookingDto.java rename to server/src/main/java/ru/practicum/shareit/booking/dto/SimplifiedBookingDto.java diff --git a/src/main/java/ru/practicum/shareit/booking/dto/mapper/BookingMapper.java b/server/src/main/java/ru/practicum/shareit/booking/dto/mapper/BookingMapper.java similarity index 96% rename from src/main/java/ru/practicum/shareit/booking/dto/mapper/BookingMapper.java rename to server/src/main/java/ru/practicum/shareit/booking/dto/mapper/BookingMapper.java index e015d27..2c106f0 100644 --- a/src/main/java/ru/practicum/shareit/booking/dto/mapper/BookingMapper.java +++ b/server/src/main/java/ru/practicum/shareit/booking/dto/mapper/BookingMapper.java @@ -17,7 +17,8 @@ public static BookingResponseDto toBookingDto(Booking booking) { ItemDto itemDto = new ItemDto(booking.getItem().getId(), booking.getItem().getName(), booking.getItem().getDescription(), - booking.getItem().getAvailable() + booking.getItem().getAvailable(), + null ); UserDto bookerDto = new UserDto( diff --git a/src/main/java/ru/practicum/shareit/booking/model/Booking.java b/server/src/main/java/ru/practicum/shareit/booking/model/Booking.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/model/Booking.java rename to server/src/main/java/ru/practicum/shareit/booking/model/Booking.java diff --git a/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java b/server/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java similarity index 100% rename from src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java rename to server/src/main/java/ru/practicum/shareit/booking/repository/BookingRepository.java diff --git a/src/main/java/ru/practicum/shareit/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/server/src/main/java/ru/practicum/shareit/booking/status/RentalStatus.java b/server/src/main/java/ru/practicum/shareit/booking/status/RentalStatus.java new file mode 100644 index 0000000..a36dfb1 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/status/RentalStatus.java @@ -0,0 +1,8 @@ +package ru.practicum.shareit.booking.status; + +public enum RentalStatus { + WAITING, + APPROVED, + REJECTED, + CANCELED +} diff --git a/src/main/java/ru/practicum/shareit/error/ErrorHandler.java b/server/src/main/java/ru/practicum/shareit/error/ErrorHandler.java similarity index 100% rename from src/main/java/ru/practicum/shareit/error/ErrorHandler.java rename to server/src/main/java/ru/practicum/shareit/error/ErrorHandler.java diff --git a/src/main/java/ru/practicum/shareit/error/ErrorResponse.java b/server/src/main/java/ru/practicum/shareit/error/ErrorResponse.java similarity index 100% rename from src/main/java/ru/practicum/shareit/error/ErrorResponse.java rename to server/src/main/java/ru/practicum/shareit/error/ErrorResponse.java diff --git a/src/main/java/ru/practicum/shareit/exception/InternalServerException.java b/server/src/main/java/ru/practicum/shareit/exception/InternalServerException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/exception/InternalServerException.java rename to server/src/main/java/ru/practicum/shareit/exception/InternalServerException.java diff --git a/src/main/java/ru/practicum/shareit/exception/NotFoundException.java b/server/src/main/java/ru/practicum/shareit/exception/NotFoundException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/exception/NotFoundException.java rename to server/src/main/java/ru/practicum/shareit/exception/NotFoundException.java diff --git a/src/main/java/ru/practicum/shareit/exception/ValidationException.java b/server/src/main/java/ru/practicum/shareit/exception/ValidationException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/exception/ValidationException.java rename to server/src/main/java/ru/practicum/shareit/exception/ValidationException.java diff --git a/server/src/main/java/ru/practicum/shareit/item/comment/dto/CommentRequestDto.java b/server/src/main/java/ru/practicum/shareit/item/comment/dto/CommentRequestDto.java new file mode 100644 index 0000000..c96626f --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/comment/dto/CommentRequestDto.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.item.comment.dto; + +import lombok.*; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class CommentRequestDto { + private String text; +} diff --git a/server/src/main/java/ru/practicum/shareit/item/comment/dto/CommentResponseDto.java b/server/src/main/java/ru/practicum/shareit/item/comment/dto/CommentResponseDto.java new file mode 100644 index 0000000..798ec01 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/comment/dto/CommentResponseDto.java @@ -0,0 +1,18 @@ +package ru.practicum.shareit.item.comment.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@AllArgsConstructor +@Getter +@Setter +public class CommentResponseDto { + private Long id; + private String text; + private String authorName; + private LocalDateTime created; + private Long itemId; +} diff --git a/src/main/java/ru/practicum/shareit/item/comment/dto/mapper/CommentMapper.java b/server/src/main/java/ru/practicum/shareit/item/comment/dto/mapper/CommentMapper.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/comment/dto/mapper/CommentMapper.java rename to server/src/main/java/ru/practicum/shareit/item/comment/dto/mapper/CommentMapper.java diff --git a/src/main/java/ru/practicum/shareit/item/comment/model/Comment.java b/server/src/main/java/ru/practicum/shareit/item/comment/model/Comment.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/comment/model/Comment.java rename to server/src/main/java/ru/practicum/shareit/item/comment/model/Comment.java diff --git a/src/main/java/ru/practicum/shareit/item/comment/repository/CommentRepository.java b/server/src/main/java/ru/practicum/shareit/item/comment/repository/CommentRepository.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/comment/repository/CommentRepository.java rename to server/src/main/java/ru/practicum/shareit/item/comment/repository/CommentRepository.java diff --git a/src/main/java/ru/practicum/shareit/item/controller/ItemController.java b/server/src/main/java/ru/practicum/shareit/item/controller/ItemController.java similarity index 91% rename from src/main/java/ru/practicum/shareit/item/controller/ItemController.java rename to server/src/main/java/ru/practicum/shareit/item/controller/ItemController.java index a0293d0..3edbaee 100644 --- a/src/main/java/ru/practicum/shareit/item/controller/ItemController.java +++ b/server/src/main/java/ru/practicum/shareit/item/controller/ItemController.java @@ -1,6 +1,5 @@ package ru.practicum.shareit.item.controller; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; @@ -20,7 +19,7 @@ public class ItemController { private final ItemService itemService; @PostMapping - public ItemDto createItem(@RequestHeader("X-Sharer-User-Id") Long userId, @Valid @RequestBody ItemDto itemDto) { + public ItemDto createItem(@RequestHeader("X-Sharer-User-Id") Long userId, @RequestBody ItemDto itemDto) { log.info("POST /items"); return itemService.createItem(userId, itemDto); } @@ -52,7 +51,7 @@ public List searchItem(@RequestParam(name = "text") String text) { @PostMapping("/{itemId}/comment") public CommentResponseDto createComment(@RequestHeader("X-Sharer-User-Id") Long userId, @PathVariable("itemId") Long itemId, - @Valid @RequestBody CommentRequestDto commentRequestDto) { + @RequestBody CommentRequestDto commentRequestDto) { log.info("POST /items/{}/comment", itemId); return itemService.createComment(userId, itemId, commentRequestDto); } diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemBookingDto.java b/server/src/main/java/ru/practicum/shareit/item/dto/ItemBookingDto.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/dto/ItemBookingDto.java rename to server/src/main/java/ru/practicum/shareit/item/dto/ItemBookingDto.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..8640224 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -0,0 +1,16 @@ +package ru.practicum.shareit.item.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@AllArgsConstructor +@Getter +@Setter +public class ItemDto { + private Long id; + private String name; + private String description; + private Boolean available; + private Long requestId; +} diff --git a/src/main/java/ru/practicum/shareit/item/dto/mapper/ItemMapper.java b/server/src/main/java/ru/practicum/shareit/item/dto/mapper/ItemMapper.java similarity index 73% rename from src/main/java/ru/practicum/shareit/item/dto/mapper/ItemMapper.java rename to server/src/main/java/ru/practicum/shareit/item/dto/mapper/ItemMapper.java index 72cbccd..bc1f756 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/mapper/ItemMapper.java +++ b/server/src/main/java/ru/practicum/shareit/item/dto/mapper/ItemMapper.java @@ -6,6 +6,7 @@ import ru.practicum.shareit.item.dto.ItemBookingDto; import ru.practicum.shareit.item.dto.ItemDto; import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.request.model.ItemRequest; import ru.practicum.shareit.user.model.User; import java.util.List; @@ -17,7 +18,8 @@ public static ItemDto toItemDto(Item item) { item.getId(), item.getName(), item.getDescription(), - item.getAvailable() + item.getAvailable(), + item.getItemRequest() != null ? item.getItemRequest().getId() : null ); } @@ -27,7 +29,19 @@ public static Item toItem(User user, ItemDto itemDto) { itemDto.getName(), itemDto.getDescription(), itemDto.getAvailable(), - user + user, + null + ); + } + + public static Item toItem(User user, ItemDto itemDto, ItemRequest itemRequest) { + return new Item( + null, + itemDto.getName(), + itemDto.getDescription(), + itemDto.getAvailable(), + user, + itemRequest ); } 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 74% 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 bf8fe78..ded119f 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,6 +2,7 @@ import jakarta.persistence.*; import lombok.*; +import ru.practicum.shareit.request.model.ItemRequest; import ru.practicum.shareit.user.model.User; @NoArgsConstructor @@ -20,4 +21,7 @@ public class Item { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "owner_id") private User owner; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "item_request_id") + private ItemRequest itemRequest; } diff --git a/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java b/server/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java similarity index 90% rename from src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java rename to server/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java index b71ebeb..719cd63 100644 --- a/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java +++ b/server/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java @@ -14,4 +14,6 @@ public interface ItemRepository extends JpaRepository { "WHERE UPPER(i.name) LIKE UPPER(CONCAT('%', ?1, '%')) " + " OR UPPER(i.description) LIKE UPPER(CONCAT('%', ?1, '%'))") List search(String text); + + List findAllByItemRequestIdIn(List requestIds); } 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 89% 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 33c7743..872be0d 100644 --- a/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java +++ b/server/src/main/java/ru/practicum/shareit/item/service/ItemServiceImpl.java @@ -18,6 +18,8 @@ import ru.practicum.shareit.item.dto.ItemDto; import ru.practicum.shareit.item.dto.mapper.ItemMapper; import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.request.repository.ItemRequestRepository; import ru.practicum.shareit.user.model.User; import ru.practicum.shareit.user.repository.UserRepository; @@ -34,14 +36,20 @@ public class ItemServiceImpl implements ItemService { private final UserRepository userRepository; private final BookingRepository bookingRepository; private final CommentRepository commentRepository; + private final ItemRequestRepository itemRequestRepository; @Override public ItemDto createItem(Long userId, ItemDto itemDto) { User user = validateUser(userId); - return ItemMapper.toItemDto(itemRepository.save( - ItemMapper.toItem(user, itemDto)) - ); + if (itemDto.getRequestId() != null) { + ItemRequest itemRequest = validateItemRequest(itemDto.getRequestId()); + return ItemMapper.toItemDto(itemRepository.save( + ItemMapper.toItem(user, itemDto, itemRequest))); + } else { + return ItemMapper.toItemDto(itemRepository.save( + ItemMapper.toItem(user, itemDto))); + } } @Override @@ -174,4 +182,9 @@ private Item validateItem(Long itemId) { return itemRepository.findById(itemId) .orElseThrow(() -> new NotFoundException("Элемент с данным id: " + itemId + " не найден")); } + + private ItemRequest validateItemRequest(Long requestId) { + return itemRequestRepository.findById(requestId) + .orElseThrow(() -> new NotFoundException("Запрос вещи с данным id: " + requestId + " не найден")); + } } diff --git a/server/src/main/java/ru/practicum/shareit/request/controller/ItemRequestController.java b/server/src/main/java/ru/practicum/shareit/request/controller/ItemRequestController.java new file mode 100644 index 0000000..1461442 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/controller/ItemRequestController.java @@ -0,0 +1,44 @@ +package ru.practicum.shareit.request.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.request.dto.ItemReqRequestDto; +import ru.practicum.shareit.request.dto.ItemReqResponseDto; +import ru.practicum.shareit.request.service.ItemRequestService; + +import java.util.List; + +@RequiredArgsConstructor +@Slf4j +@RestController +@RequestMapping(path = "/requests") +public class ItemRequestController { + private final ItemRequestService itemRequestService; + + @PostMapping + public ItemReqResponseDto createItemRequest(@RequestHeader("X-Sharer-User-Id") Long userId, @RequestBody ItemReqRequestDto itemReqRequestDto) { + log.info("POST /requests"); + return itemRequestService.createItemRequest(userId, itemReqRequestDto); + } + + @GetMapping + public List getRequests(@RequestHeader("X-Sharer-User-Id") Long userId) { + log.info("GET /requests"); + return itemRequestService.getRequests(userId); + } + + @GetMapping("/all") + public List getRequestsOtherUsers(@RequestHeader("X-Sharer-User-Id") Long userId, + @RequestParam(defaultValue = "0") int from, + @RequestParam(defaultValue = "10") int size) { + log.info("GET /requests/all"); + return itemRequestService.getRequestsOtherUsers(userId, from, size); + } + + @GetMapping("/{requestId}") + public ItemReqResponseDto getRequestWithAnswers(@RequestHeader("X-Sharer-User-Id") Long userId, @PathVariable Long requestId) { + log.info("GET /requests/{}", requestId); + return itemRequestService.getRequestWithAnswers(userId, requestId); + } +} diff --git a/server/src/main/java/ru/practicum/shareit/request/dto/ItemReqRequestDto.java b/server/src/main/java/ru/practicum/shareit/request/dto/ItemReqRequestDto.java new file mode 100644 index 0000000..62ced4c --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/dto/ItemReqRequestDto.java @@ -0,0 +1,15 @@ +package ru.practicum.shareit.request.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Setter +@Getter +public class ItemReqRequestDto { + private String description; + private Long requestorId; +} diff --git a/server/src/main/java/ru/practicum/shareit/request/dto/ItemReqResponseDto.java b/server/src/main/java/ru/practicum/shareit/request/dto/ItemReqResponseDto.java new file mode 100644 index 0000000..ac1de19 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/dto/ItemReqResponseDto.java @@ -0,0 +1,22 @@ +package ru.practicum.shareit.request.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.shareit.item.dto.ItemDto; + +import java.time.LocalDateTime; +import java.util.List; + +@NoArgsConstructor +@AllArgsConstructor +@Setter +@Getter +public class ItemReqResponseDto { + private Long id; + private String description; + private Long userId; + private LocalDateTime created; + private List items; +} diff --git a/server/src/main/java/ru/practicum/shareit/request/dto/mapper/ItemRequestMapper.java b/server/src/main/java/ru/practicum/shareit/request/dto/mapper/ItemRequestMapper.java new file mode 100644 index 0000000..9672b99 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/dto/mapper/ItemRequestMapper.java @@ -0,0 +1,33 @@ +package ru.practicum.shareit.request.dto.mapper; + +import lombok.experimental.UtilityClass; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.request.dto.ItemReqRequestDto; +import ru.practicum.shareit.request.dto.ItemReqResponseDto; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.List; + +@UtilityClass +public class ItemRequestMapper { + public static ItemReqResponseDto toItemResponseDto(ItemRequest itemRequest, List items) { + return new ItemReqResponseDto( + itemRequest.getId(), + itemRequest.getDescription(), + itemRequest.getRequestor().getId(), + itemRequest.getCreated(), + items + ); + } + + public static ItemRequest toItemRequest(ItemReqRequestDto itemReqRequestDto, User user) { + return new ItemRequest( + null, + itemReqRequestDto.getDescription(), + user, + LocalDateTime.now() + ); + } +} diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequest.java b/server/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java similarity index 54% rename from src/main/java/ru/practicum/shareit/request/ItemRequest.java rename to server/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java index 89006f7..2269eca 100644 --- a/src/main/java/ru/practicum/shareit/request/ItemRequest.java +++ b/server/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java @@ -1,21 +1,27 @@ -package ru.practicum.shareit.request; +package ru.practicum.shareit.request.model; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import ru.practicum.shareit.user.model.User; import java.time.LocalDateTime; -/** - * TODO Sprint add-item-requests. - */ +@Entity +@Table(name = "requests") +@NoArgsConstructor @AllArgsConstructor @Getter @Setter public class ItemRequest { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String description; + @ManyToOne + @JoinColumn(name = "requestor_id") private User requestor; private LocalDateTime created; } diff --git a/server/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java b/server/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java new file mode 100644 index 0000000..86ae310 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/repository/ItemRequestRepository.java @@ -0,0 +1,14 @@ +package ru.practicum.shareit.request.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.shareit.request.model.ItemRequest; + +import java.util.List; + +public interface ItemRequestRepository extends JpaRepository { + List findByRequestorIdOrderByCreatedDesc(Long requestorId); + + Page findByRequestorIdNot(Long requestorId, Pageable pageable); +} diff --git a/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java new file mode 100644 index 0000000..cee70e5 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestService.java @@ -0,0 +1,16 @@ +package ru.practicum.shareit.request.service; + +import ru.practicum.shareit.request.dto.ItemReqRequestDto; +import ru.practicum.shareit.request.dto.ItemReqResponseDto; + +import java.util.List; + +public interface ItemRequestService { + ItemReqResponseDto createItemRequest(Long userId, ItemReqRequestDto itemReqRequestDto); + + List getRequests(Long userId); + + List getRequestsOtherUsers(Long userId, int from, int size); + + ItemReqResponseDto getRequestWithAnswers(Long userId, Long requestId); +} diff --git a/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java new file mode 100644 index 0000000..a3ea652 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/service/ItemRequestServiceImpl.java @@ -0,0 +1,108 @@ +package ru.practicum.shareit.request.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.mapper.ItemMapper; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.request.dto.ItemReqRequestDto; +import ru.practicum.shareit.request.dto.ItemReqResponseDto; +import ru.practicum.shareit.request.dto.mapper.ItemRequestMapper; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.request.repository.ItemRequestRepository; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Service +public class ItemRequestServiceImpl implements ItemRequestService { + private final UserRepository userRepository; + private final ItemRequestRepository itemRequestRepository; + private final ItemRepository itemRepository; + + @Override + public ItemReqResponseDto createItemRequest(Long userId, ItemReqRequestDto itemReqRequestDto) { + User user = validateUser(userId); + + return ItemRequestMapper.toItemResponseDto(itemRequestRepository + .save(ItemRequestMapper.toItemRequest(itemReqRequestDto, user)), List.of()); + } + + @Override + public List getRequests(Long userId) { + validateUser(userId); + + List requests = itemRequestRepository.findByRequestorIdOrderByCreatedDesc(userId); + + return composeResponseForItemResponseDto(requests); + } + + @Override + public List getRequestsOtherUsers(Long userId, int from, int size) { + validateUser(userId); + + Pageable pageable = PageRequest.of(from / size, size, Sort.by("created").descending()); + + List requests = itemRequestRepository.findByRequestorIdNot(userId, pageable).getContent(); + + return composeResponseForItemResponseDto(requests); + } + + @Override + public ItemReqResponseDto getRequestWithAnswers(Long userId, Long requestId) { + validateUser(userId); + + ItemRequest itemRequest = itemRequestRepository.findById(requestId) + .orElseThrow(() -> new NotFoundException("Запрос вещи с данным id: " + requestId + " не найден")); + + List items = itemRepository.findAllByItemRequestIdIn(List.of(itemRequest.getId())); + + return ItemRequestMapper.toItemResponseDto(itemRequest, + items + .stream() + .map(ItemMapper::toItemDto) + .collect(Collectors.toList())); + } + + private User validateUser(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException("Пользователь с данным id: " + userId + " не найден")); + } + + private List composeResponseForItemResponseDto(List requests) { + if (requests == null || requests.isEmpty()) { + return List.of(); + } + + List requestIds = requests + .stream() + .map(ItemRequest::getId) + .collect(Collectors.toList()); + + if (requestIds.isEmpty()) return List.of(); + + List items = itemRepository.findAllByItemRequestIdIn(requestIds); + + Map> itemsByRequest = items + .stream() + .map(ItemMapper::toItemDto) + .collect(Collectors.groupingBy(ItemDto::getRequestId)); + + return requests.stream() + .map(req -> ItemRequestMapper.toItemResponseDto( + req, + itemsByRequest.getOrDefault(req.getId(), List.of()) + )) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/ru/practicum/shareit/user/controller/UserController.java b/server/src/main/java/ru/practicum/shareit/user/controller/UserController.java similarity index 92% rename from src/main/java/ru/practicum/shareit/user/controller/UserController.java rename to server/src/main/java/ru/practicum/shareit/user/controller/UserController.java index b8c34a3..5cd25b5 100644 --- a/src/main/java/ru/practicum/shareit/user/controller/UserController.java +++ b/server/src/main/java/ru/practicum/shareit/user/controller/UserController.java @@ -1,6 +1,5 @@ package ru.practicum.shareit.user.controller; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; @@ -30,7 +29,7 @@ public UserDto findUserById(@PathVariable("id") Long id) { } @PostMapping - public UserDto createUser(@Valid @RequestBody UserDto userDto) { + public UserDto createUser(@RequestBody UserDto userDto) { log.info("POST /users"); return userService.createUser(userDto); } 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..364c74b --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/dto/UserDto.java @@ -0,0 +1,13 @@ +package ru.practicum.shareit.user.dto; + +import lombok.*; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class UserDto { + private Long id; + private String name; + private String email; +} diff --git a/src/main/java/ru/practicum/shareit/user/dto/mapper/UserMapper.java b/server/src/main/java/ru/practicum/shareit/user/dto/mapper/UserMapper.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/dto/mapper/UserMapper.java rename to server/src/main/java/ru/practicum/shareit/user/dto/mapper/UserMapper.java diff --git a/src/main/java/ru/practicum/shareit/user/model/User.java b/server/src/main/java/ru/practicum/shareit/user/model/User.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/model/User.java rename to server/src/main/java/ru/practicum/shareit/user/model/User.java diff --git a/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java b/server/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/repository/UserRepository.java rename to server/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java diff --git a/src/main/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/server/src/main/resources/application.yml b/server/src/main/resources/application.yml new file mode 100644 index 0000000..8e2fa4a --- /dev/null +++ b/server/src/main/resources/application.yml @@ -0,0 +1,45 @@ +spring: + profiles: + active: dev + +--- + +spring: + config: + activate: + on-profile: dev + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://localhost:5432/shareit + username: dbuser + password: 12345 + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + +server: + port: 9090 + +--- + +spring: + config: + activate: + on-profile: test + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:shareit + username: dbuser + password: 12345 + jpa: + hibernate: + ddl-auto: create-drop + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect + +server: + port: 9090 \ No newline at end of file diff --git a/src/main/resources/schema.sql b/server/src/main/resources/schema.sql similarity index 91% rename from src/main/resources/schema.sql rename to server/src/main/resources/schema.sql index fa318ff..430494b 100644 --- a/src/main/resources/schema.sql +++ b/server/src/main/resources/schema.sql @@ -12,14 +12,25 @@ create TABLE IF NOT EXISTS users ( CONSTRAINT UQ_USER_EMAIL UNIQUE (email) ); +create TABLE IF NOT EXISTS requests ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + description VARCHAR(2000), + requestor_id BIGINT NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + CONSTRAINT pk_request PRIMARY KEY (id), + CONSTRAINT fk_request_requestor FOREIGN KEY (requestor_id) REFERENCES users(id) +); + create TABLE IF NOT EXISTS items ( id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, description VARCHAR(2000), available BOOLEAN DEFAULT TRUE, owner_id BIGINT NOT NULL, + item_request_id BIGINT, CONSTRAINT pk_item PRIMARY KEY (id), - CONSTRAINT fk_item_owner FOREIGN KEY (owner_id) REFERENCES users(id) + CONSTRAINT fk_item_owner FOREIGN KEY (owner_id) REFERENCES users(id), + CONSTRAINT fk_item_item_request FOREIGN KEY (item_request_id) REFERENCES requests(id) ); create TABLE IF NOT EXISTS bookings ( @@ -34,14 +45,6 @@ create TABLE IF NOT EXISTS bookings ( CONSTRAINT fk_booking_booker FOREIGN KEY (booker_id) REFERENCES users(id) ); -create TABLE IF NOT EXISTS requests ( - id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, - description VARCHAR(2000), - requestor_id BIGINT NOT NULL, - CONSTRAINT pk_request PRIMARY KEY (id), - CONSTRAINT fk_request_requestor FOREIGN KEY (requestor_id) REFERENCES users(id) -); - create TABLE IF NOT EXISTS comments ( id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, text VARCHAR(2000), diff --git a/src/test/java/ru/practicum/shareit/ShareItTests.java b/server/src/test/java/ru/practicum/shareit/ShareItTests.java similarity index 80% rename from src/test/java/ru/practicum/shareit/ShareItTests.java rename to server/src/test/java/ru/practicum/shareit/ShareItTests.java index 6dd7261..8502acd 100644 --- a/src/test/java/ru/practicum/shareit/ShareItTests.java +++ b/server/src/test/java/ru/practicum/shareit/ShareItTests.java @@ -1,4 +1,3 @@ -/* package ru.practicum.shareit; import org.junit.jupiter.api.Test; @@ -7,9 +6,8 @@ @SpringBootTest class ShareItTests { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } } -*/ diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java new file mode 100644 index 0000000..db60a85 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java @@ -0,0 +1,293 @@ +package ru.practicum.shareit.booking; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +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.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.shareit.booking.controller.BookingController; +import ru.practicum.shareit.booking.dto.BookingRequestDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; +import ru.practicum.shareit.booking.service.BookingService; +import ru.practicum.shareit.booking.status.RentalStatus; +import ru.practicum.shareit.error.ErrorHandler; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.user.dto.UserDto; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +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; + +@ExtendWith(MockitoExtension.class) +public class BookingControllerTest { + @Mock + private BookingService bookingService; + @InjectMocks + private BookingController bookingController; + private final ObjectMapper mapper = new ObjectMapper(); + private MockMvc mvc; + private BookingRequestDto bookingRequestDto; + private ItemDto itemDto; + private UserDto userDto; + private BookingResponseDto bookingResponseDto; + private BookingResponseDto bookingResponseDto2; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + + mvc = MockMvcBuilders + .standaloneSetup(bookingController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + bookingRequestDto = new BookingRequestDto( + 1L, + LocalDateTime.of(2025, 7, 20, 10, 0), + LocalDateTime.of(2025, 7, 20, 12, 0) + ); + + userDto = new UserDto( + 1L, + "Макс Иванов", + "Max@mail.ru" + ); + + itemDto = new ItemDto( + 1L, + "Canon 500d", + "Полупрофессиональная модель фотоаппарата", + true, + 5L + ); + + bookingResponseDto = new BookingResponseDto( + 1L, + LocalDateTime.of(2025, 7, 20, 10, 0), + LocalDateTime.of(2025, 7, 20, 12, 0), + itemDto, + userDto, + RentalStatus.APPROVED + ); + + bookingResponseDto2 = new BookingResponseDto( + 2L, + LocalDateTime.of(2025, 7, 20, 13, 0), + LocalDateTime.of(2025, 7, 20, 14, 0), + null, + null, + RentalStatus.APPROVED + ); + } + + @Test + void mustCreateBooking() throws Exception { + when(bookingService.createBooking(eq(1L), any())) + .thenReturn(bookingResponseDto); + + mvc.perform(post("/bookings") + .header("X-Sharer-User-Id", 1L) + .content(mapper.writeValueAsString(bookingRequestDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.start[0]", is(2025))) + .andExpect(jsonPath("$.start[1]", is(7))) + .andExpect(jsonPath("$.start[2]", is(20))) + .andExpect(jsonPath("$.start[3]", is(10))) + .andExpect(jsonPath("$.start[4]", is(0))) + .andExpect(jsonPath("$.end[0]", is(2025))) + .andExpect(jsonPath("$.end[1]", is(7))) + .andExpect(jsonPath("$.end[2]", is(20))) + .andExpect(jsonPath("$.end[3]", is(12))) + .andExpect(jsonPath("$.end[4]", is(0))) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.item.name", is("Canon 500d"))) + .andExpect(jsonPath("$.item.description", is("Полупрофессиональная модель фотоаппарата"))) + .andExpect(jsonPath("$.item.available", is(true))) + .andExpect(jsonPath("$.item.requestId", is(5))) + .andExpect(jsonPath("$.booker.id", is(1))) + .andExpect(jsonPath("$.booker.name", is("Макс Иванов"))) + .andExpect(jsonPath("$.booker.email", is("Max@mail.ru"))) + .andExpect(jsonPath("$.status", is("APPROVED"))); + } + + @Test + void mustUpdateBookingStatus() throws Exception { + bookingResponseDto.setStatus(RentalStatus.REJECTED); + + when(bookingService.updateBooking(1L, 1L, false)) + .thenReturn(bookingResponseDto); + + mvc.perform(post("/bookings/1") + .param("approved", "false") + .header("X-Sharer-User-Id", "1") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .with(request -> { + request.setMethod("PATCH"); + return request; + })) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.start[0]", is(2025))) + .andExpect(jsonPath("$.start[1]", is(7))) + .andExpect(jsonPath("$.start[2]", is(20))) + .andExpect(jsonPath("$.start[3]", is(10))) + .andExpect(jsonPath("$.start[4]", is(0))) + .andExpect(jsonPath("$.end[0]", is(2025))) + .andExpect(jsonPath("$.end[1]", is(7))) + .andExpect(jsonPath("$.end[2]", is(20))) + .andExpect(jsonPath("$.end[3]", is(12))) + .andExpect(jsonPath("$.end[4]", is(0))) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.item.name", is("Canon 500d"))) + .andExpect(jsonPath("$.item.description", is("Полупрофессиональная модель фотоаппарата"))) + .andExpect(jsonPath("$.item.available", is(true))) + .andExpect(jsonPath("$.item.requestId", is(5))) + .andExpect(jsonPath("$.booker.id", is(1))) + .andExpect(jsonPath("$.booker.name", is("Макс Иванов"))) + .andExpect(jsonPath("$.booker.email", is("Max@mail.ru"))) + .andExpect(jsonPath("$.status", is("REJECTED"))); + } + + @Test + void mustReturnBookingById() throws Exception { + when(bookingService.findBookingById(1L, 1L)) + .thenReturn(bookingResponseDto); + + mvc.perform(get("/bookings/1") + .header("X-Sharer-User-Id", "1") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.start[0]", is(2025))) + .andExpect(jsonPath("$.start[1]", is(7))) + .andExpect(jsonPath("$.start[2]", is(20))) + .andExpect(jsonPath("$.start[3]", is(10))) + .andExpect(jsonPath("$.start[4]", is(0))) + .andExpect(jsonPath("$.end[0]", is(2025))) + .andExpect(jsonPath("$.end[1]", is(7))) + .andExpect(jsonPath("$.end[2]", is(20))) + .andExpect(jsonPath("$.end[3]", is(12))) + .andExpect(jsonPath("$.end[4]", is(0))) + .andExpect(jsonPath("$.item.name", is("Canon 500d"))) + .andExpect(jsonPath("$.item.description", is("Полупрофессиональная модель фотоаппарата"))) + .andExpect(jsonPath("$.item.available", is(true))) + .andExpect(jsonPath("$.item.requestId", is(5))) + .andExpect(jsonPath("$.booker.id", is(1))) + .andExpect(jsonPath("$.booker.name", is("Макс Иванов"))) + .andExpect(jsonPath("$.booker.email", is("Max@mail.ru"))) + .andExpect(jsonPath("$.status", is("APPROVED"))); + } + + @Test + void mustReturnUserBookings() throws Exception { + List bookings = List.of(bookingResponseDto, bookingResponseDto2); + + when(bookingService.findUserBookings(1L, "ALL")) + .thenReturn(bookings); + + mvc.perform(get("/bookings") + .header("X-Sharer-User-Id", 1L) + .param("state", "ALL") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id", is(1))) + .andExpect(jsonPath("$[0].start[0]", is(2025))) + .andExpect(jsonPath("$[0].start[1]", is(7))) + .andExpect(jsonPath("$[0].start[2]", is(20))) + .andExpect(jsonPath("$[0].start[3]", is(10))) + .andExpect(jsonPath("$[0].start[4]", is(0))) + .andExpect(jsonPath("$[0].end[0]", is(2025))) + .andExpect(jsonPath("$[0].end[1]", is(7))) + .andExpect(jsonPath("$[0].end[2]", is(20))) + .andExpect(jsonPath("$[0].end[3]", is(12))) + .andExpect(jsonPath("$[0].end[4]", is(0))) + .andExpect(jsonPath("$[0].item.name", is("Canon 500d"))) + .andExpect(jsonPath("$[0].item.description", is("Полупрофессиональная модель фотоаппарата"))) + .andExpect(jsonPath("$[0].item.available", is(true))) + .andExpect(jsonPath("$[0].item.requestId", is(5))) + .andExpect(jsonPath("$[0].booker.id", is(1))) + .andExpect(jsonPath("$[0].booker.name", is("Макс Иванов"))) + .andExpect(jsonPath("$[0].booker.email", is("Max@mail.ru"))) + .andExpect(jsonPath("$[0].status", is("APPROVED"))) + .andExpect(jsonPath("$[1].id", is(2))) + .andExpect(jsonPath("$[1].start[0]", is(2025))) + .andExpect(jsonPath("$[1].start[1]", is(7))) + .andExpect(jsonPath("$[1].start[2]", is(20))) + .andExpect(jsonPath("$[1].start[3]", is(13))) + .andExpect(jsonPath("$[1].start[4]", is(0))) + .andExpect(jsonPath("$[1].end[0]", is(2025))) + .andExpect(jsonPath("$[1].end[1]", is(7))) + .andExpect(jsonPath("$[1].end[2]", is(20))) + .andExpect(jsonPath("$[1].end[3]", is(14))) + .andExpect(jsonPath("$[1].end[4]", is(0))) + .andExpect(jsonPath("$[1].item").doesNotExist()) + .andExpect(jsonPath("$[1].booker").doesNotExist()) + .andExpect(jsonPath("$[1].status", is("APPROVED"))); + } + + @Test + void mustReturnOwnerReservedItems() throws Exception { + List bookings = List.of(bookingResponseDto, bookingResponseDto2); + + when(bookingService.findOwnerReservedItems(1L, "ALL")) + .thenReturn(bookings); + + mvc.perform(get("/bookings/owner") + .header("X-Sharer-User-Id", 1L) + .param("state", "ALL") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id", is(1))) + .andExpect(jsonPath("$[0].start[0]", is(2025))) + .andExpect(jsonPath("$[0].start[1]", is(7))) + .andExpect(jsonPath("$[0].start[2]", is(20))) + .andExpect(jsonPath("$[0].start[3]", is(10))) + .andExpect(jsonPath("$[0].start[4]", is(0))) + .andExpect(jsonPath("$[0].end[0]", is(2025))) + .andExpect(jsonPath("$[0].end[1]", is(7))) + .andExpect(jsonPath("$[0].end[2]", is(20))) + .andExpect(jsonPath("$[0].end[3]", is(12))) + .andExpect(jsonPath("$[0].end[4]", is(0))) + .andExpect(jsonPath("$[0].item.name", is("Canon 500d"))) + .andExpect(jsonPath("$[0].item.description", is("Полупрофессиональная модель фотоаппарата"))) + .andExpect(jsonPath("$[0].item.available", is(true))) + .andExpect(jsonPath("$[0].item.requestId", is(5))) + .andExpect(jsonPath("$[0].booker.id", is(1))) + .andExpect(jsonPath("$[0].booker.name", is("Макс Иванов"))) + .andExpect(jsonPath("$[0].booker.email", is("Max@mail.ru"))) + .andExpect(jsonPath("$[0].status", is("APPROVED"))) + .andExpect(jsonPath("$[1].id", is(2))) + .andExpect(jsonPath("$[1].start[0]", is(2025))) + .andExpect(jsonPath("$[1].start[1]", is(7))) + .andExpect(jsonPath("$[1].start[2]", is(20))) + .andExpect(jsonPath("$[1].start[3]", is(13))) + .andExpect(jsonPath("$[1].start[4]", is(0))) + .andExpect(jsonPath("$[1].end[0]", is(2025))) + .andExpect(jsonPath("$[1].end[1]", is(7))) + .andExpect(jsonPath("$[1].end[2]", is(20))) + .andExpect(jsonPath("$[1].end[3]", is(14))) + .andExpect(jsonPath("$[1].end[4]", is(0))) + .andExpect(jsonPath("$[1].item").doesNotExist()) + .andExpect(jsonPath("$[1].booker").doesNotExist()) + .andExpect(jsonPath("$[1].status", is("APPROVED"))); + } +} 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..6e34bc7 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingMapperTest.java @@ -0,0 +1,100 @@ +package ru.practicum.shareit.booking; + +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.booking.dto.BookingRequestDto; +import ru.practicum.shareit.booking.dto.SimplifiedBookingDto; +import ru.practicum.shareit.booking.dto.mapper.BookingMapper; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.status.RentalStatus; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class BookingMapperTest { + @Test + void shouldMapToBooking() { + LocalDateTime start = LocalDateTime.of(2025, 7, 21, 10, 0); + LocalDateTime end = LocalDateTime.of(2025, 7, 22, 10, 0); + + BookingRequestDto requestDto = new BookingRequestDto( + 1L, + start, + end + ); + + User booker = new User( + 1L, + "Макc Иванов", + "Max@mail.ru" + ); + + User owner = new User( + 2L, + "Петр Петров", + "Petr@mail.ru" + ); + + Item item = new Item( + 3L, + "Canon 500d", + "Зеркальный фотоаппарат", + true, + owner, + null + ); + + RentalStatus status = RentalStatus.WAITING; + + Booking booking = BookingMapper.toBooking(requestDto, item, booker, status); + + assertThat(booking.getId()).isNull(); + assertThat(booking.getStart()).isEqualTo(start); + assertThat(booking.getEnd()).isEqualTo(end); + assertThat(booking.getItem()).isEqualTo(item); + assertThat(booking.getBooker()).isEqualTo(booker); + assertThat(booking.getStatus()).isEqualTo(RentalStatus.WAITING); + } + + @Test + void mustMapToSimplifiedBookingDto() { + User owner = new User( + 1L, + "Макс Иванов", + "Max@mail.ru" + ); + + Item item = new Item( + 3L, + "Canon 500d", + "Зеркальный фотоаппарат", + true, + owner, + null + ); + Booking booking = new Booking( + 10L, + LocalDateTime.now(), + LocalDateTime.now().plusDays(1), + item, + owner, + RentalStatus.APPROVED + ); + + SimplifiedBookingDto dto = BookingMapper.toSimplifiedBookingDto(booking); + + assertThat(dto.getId()).isEqualTo(10L); + assertThat(dto.getBookerId()).isEqualTo(1L); + } + + @Test + void mustReturnNullSimplifiedBookingDto() { + SimplifiedBookingDto simplifiedBookingDto = BookingMapper.toSimplifiedBookingDto(null); + + assertThat(simplifiedBookingDto).isNull(); + } +} + + diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingRequestAndResponseDtoTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingRequestAndResponseDtoTest.java new file mode 100644 index 0000000..0804bd5 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingRequestAndResponseDtoTest.java @@ -0,0 +1,114 @@ +package ru.practicum.shareit.booking; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.shareit.ShareItApp; +import ru.practicum.shareit.booking.dto.BookingRequestDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; +import ru.practicum.shareit.booking.dto.SimplifiedBookingDto; +import ru.practicum.shareit.booking.status.RentalStatus; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.user.dto.UserDto; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@ContextConfiguration(classes = ShareItApp.class) +@JsonTest +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class BookingRequestAndResponseDtoTest { + private final JacksonTester jsonBookingRequestDto; + private final JacksonTester jsonBookingResponseDto; + private final JacksonTester jsonItemDto; + private final JacksonTester jsonUserDto; + private final JacksonTester jsonSimplifiedBookingDto; + + @Test + void shouldReturnBookingRequestDto() throws Exception { + BookingRequestDto bookingRequestDto = new BookingRequestDto( + 1L, + LocalDateTime.of(2025, 7, 20, 10, 0), + LocalDateTime.of(2025, 7, 20, 12, 0) + ); + JsonContent result = jsonBookingRequestDto.write(bookingRequestDto); + + assertThat(result).extractingJsonPathNumberValue("$.itemId").isEqualTo(1); + assertThat(result).extractingJsonPathStringValue("$.start").isEqualTo("2025-07-20T10:00:00"); + assertThat(result).extractingJsonPathStringValue("$.end").isEqualTo("2025-07-20T12:00:00"); + } + + @Test + void shouldReturnBookingResponseDto() throws Exception { + UserDto userDto = new UserDto( + 1L, + "Макс Иванов", + "Max@mail.ru" + ); + + ItemDto itemDto = new ItemDto( + 1L, + "Canon 500d", + "Полупрофессиональная модель фотоаппарата", + true, + 5L + ); + + BookingResponseDto bookingResponseDto = new BookingResponseDto( + 1L, + LocalDateTime.of(2025, 7, 20, 10, 0), + LocalDateTime.of(2025, 7, 20, 12, 0), + itemDto, + userDto, + RentalStatus.APPROVED + ); + + JsonContent resultUserDto = jsonUserDto.write(userDto); + JsonContent resultItemDto = jsonItemDto.write(itemDto); + JsonContent resultBookingResponseDto = jsonBookingResponseDto.write(bookingResponseDto); + + assertThat(resultBookingResponseDto).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(resultBookingResponseDto).extractingJsonPathStringValue("$.start").isEqualTo("2025-07-20T10:00:00"); + assertThat(resultBookingResponseDto).extractingJsonPathStringValue("$.end").isEqualTo("2025-07-20T12:00:00"); + assertThat(resultItemDto).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(resultItemDto).extractingJsonPathStringValue("$.name").isEqualTo("Canon 500d"); + assertThat(resultItemDto).extractingJsonPathStringValue("$.description").isEqualTo("Полупрофессиональная модель фотоаппарата"); + assertThat(resultItemDto).extractingJsonPathBooleanValue("$.available").isEqualTo(true); + assertThat(resultItemDto).extractingJsonPathNumberValue("$.requestId").isEqualTo(5); + assertThat(resultUserDto).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(resultUserDto).extractingJsonPathStringValue("$.name").isEqualTo("Макс Иванов"); + assertThat(resultUserDto).extractingJsonPathStringValue("$.email").isEqualTo("Max@mail.ru"); + assertThat(resultBookingResponseDto).extractingJsonPathStringValue("$.status").isEqualTo("APPROVED"); + } + + @Test + void shouldReturnSimplifiedBookingDto() throws Exception { + SimplifiedBookingDto simplifiedBookingDto = new SimplifiedBookingDto( + 1L, + 1L + ); + + JsonContent resultSimplifiedBookingDto = jsonSimplifiedBookingDto.write(simplifiedBookingDto); + + assertThat(resultSimplifiedBookingDto).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(resultSimplifiedBookingDto).extractingJsonPathNumberValue("$.bookerId").isEqualTo(1); + } + + @Test + void shouldDeserializeBookingRequestDto() throws Exception { + String json = "{ \"itemId\": 1, \"start\": \"2025-07-20T10:00:00\", \"end\": \"2025-07-20T12:00:00\" }"; + + BookingRequestDto expected = new BookingRequestDto( + 1L, + LocalDateTime.of(2025, 7, 20, 10, 0), + LocalDateTime.of(2025, 7, 20, 12, 0) + ); + + assertThat(jsonBookingRequestDto.parse(json)).usingRecursiveComparison().isEqualTo(expected); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingServiceTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingServiceTest.java new file mode 100644 index 0000000..ab3ff17 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingServiceTest.java @@ -0,0 +1,225 @@ +package ru.practicum.shareit.booking; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.ShareItApp; +import ru.practicum.shareit.booking.dto.BookingRequestDto; +import ru.practicum.shareit.booking.dto.BookingResponseDto; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.service.BookingService; +import ru.practicum.shareit.booking.status.RentalStatus; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.exception.ValidationException; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Transactional +@ActiveProfiles("test") +@SpringBootTest( + classes = ShareItApp.class, + webEnvironment = SpringBootTest.WebEnvironment.NONE) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class BookingServiceTest { + private final EntityManager em; + private final BookingService bookingService; + + private Long ownerId; + private Long bookerId; + private Long bookingId; + private Long itemId; + + @BeforeEach + void setUp() { + User owner = new User(); + owner.setName("Владимир Петухов"); + owner.setEmail("Vladimir@mail.ru"); + em.persist(owner); + + User booker = new User(); + booker.setName("Петр Васильев"); + booker.setEmail("Petr@mail.ru"); + em.persist(booker); + + ownerId = owner.getId(); + bookerId = booker.getId(); + + Item item = new Item(); + item.setName("Canon 500d"); + item.setDescription("Зеркальный фотоаппарат"); + item.setAvailable(true); + item.setOwner(owner); + em.persist(item); + + Booking booking = new Booking(); + booking.setStart(LocalDateTime.now().plusHours(1)); + booking.setEnd(LocalDateTime.now().plusDays(1)); + booking.setItem(item); + booking.setBooker(booker); + booking.setStatus(RentalStatus.WAITING); + em.persist(booking); + + em.flush(); + em.clear(); + + ownerId = owner.getId(); + bookerId = booker.getId(); + bookingId = booking.getId(); + itemId = item.getId(); + } + + @Test + void shouldApproveBooking() { + BookingResponseDto result = bookingService.updateBooking(ownerId, bookingId, true); + + assertThat(result.getId()).isEqualTo(bookingId); + assertThat(result.getStatus()).isEqualTo(RentalStatus.APPROVED); + } + + @Test + void shouldRejectBooking() { + BookingResponseDto result = bookingService.updateBooking(ownerId, bookingId, false); + + assertThat(result.getStatus()).isEqualTo(RentalStatus.REJECTED); + } + + @Test + void shouldThrowExceptionWhenNotOwner() { + ValidationException exception = assertThrows( + ValidationException.class, + () -> bookingService.updateBooking(bookerId, bookingId, true) + ); + + assertThat(exception.getMessage()).isEqualTo("Обновить аренду может только владелец вещи"); + } + + @Test + void shouldThrowWhenStatusAlreadySet() { + bookingService.updateBooking(ownerId, bookingId, true); + + ValidationException exception = assertThrows( + ValidationException.class, + () -> bookingService.updateBooking(ownerId, bookingId, false) + ); + + assertThat(exception.getMessage()).isEqualTo("Нельзя изменить статус: бронирование уже подтверждено или отклонено"); + } + + @Test + void shouldCreateBooking() { + BookingRequestDto dto = new BookingRequestDto( + itemId, + LocalDateTime.now().plusHours(2), + LocalDateTime.now().plusDays(2) + ); + + BookingResponseDto result = bookingService.createBooking(bookerId, dto); + assertThat(result.getItem().getId()).isEqualTo(itemId); + assertThat(result.getStatus()).isEqualTo(RentalStatus.WAITING); + } + + @Test + void shouldThrowExceptionWhenBookerIsOwner() { + BookingRequestDto dto = new BookingRequestDto( + itemId, + LocalDateTime.now().plusHours(2), + LocalDateTime.now().plusDays(2) + ); + + ValidationException ve = assertThrows(ValidationException.class, + () -> bookingService.createBooking(ownerId, dto)); + assertThat(ve.getMessage()).isEqualTo("Владелец не может бронировать свою вещь"); + } + + @Test + void shouldThrowExceptionWhenItemUnavailable() { + Item item = em.find(Item.class, itemId); + item.setAvailable(false); + em.flush(); + + BookingRequestDto dto = new BookingRequestDto( + itemId, + LocalDateTime.now().plusHours(1), + LocalDateTime.now().plusDays(1) + ); + + ValidationException validationException = assertThrows(ValidationException.class, + () -> bookingService.createBooking(bookerId, dto)); + assertThat(validationException.getMessage()).isEqualTo("Данная вещь недоступна для бронирования"); + } + + @Test + void shouldThrowExceptionWhenStartAfterEnd() { + BookingRequestDto dto = new BookingRequestDto( + itemId, + LocalDateTime.now().plusDays(1), + LocalDateTime.now().plusHours(1) + ); + + ValidationException validationException = assertThrows(ValidationException.class, + () -> bookingService.createBooking(bookerId, dto)); + assertThat(validationException.getMessage()).isEqualTo("Период указан неверно"); + } + + @Test + void shouldFindBookingByBooker() { + BookingResponseDto result = bookingService.findBookingById(bookerId, bookingId); + assertThat(result.getId()).isEqualTo(bookingId); + } + + @Test + void shouldFindBookingByOwner() { + BookingResponseDto result = bookingService.findBookingById(ownerId, bookingId); + assertThat(result.getId()).isEqualTo(bookingId); + } + + @Test + void shouldThrowExceptionWhenOtherUserRequestsBooking() { + User otherUser = new User(); + otherUser.setName("Макс Иванов"); + otherUser.setEmail("Max@mail.com"); + em.persist(otherUser); + em.flush(); + + ValidationException validationException = assertThrows(ValidationException.class, + () -> bookingService.findBookingById(otherUser.getId(), bookingId)); + assertThat(validationException.getMessage()).isEqualTo("Данный пользователь не может получить информацию по бронированию"); + } + + @Test + void shouldReturnUserBookingsAll() { + List bookings = bookingService.findUserBookings(bookerId, "ALL"); + assertThat(bookings.size()).isEqualTo(1); + } + + @Test + void shouldReturnOwnerBookingsAll() { + List bookings = bookingService.findOwnerReservedItems(ownerId, "ALL"); + assertThat(bookings.size()).isEqualTo(1); + } + + @Test + void shouldThrowExceptionIfUserNotFoundInFindUserBookings() { + NotFoundException notFoundException = assertThrows(NotFoundException.class, + () -> bookingService.findUserBookings(999L, "ALL")); + assertThat(notFoundException.getMessage()).contains("Пользователь с данным id: " + 999L + " не найден"); + } + + @Test + void shouldThrowExceptionIfUserNotFoundInFindOwnerReservedItems() { + NotFoundException notFoundException = assertThrows(NotFoundException.class, + () -> bookingService.findOwnerReservedItems(999L, "ALL")); + assertThat(notFoundException.getMessage()).contains("Пользователь с данным id: " + 999L + " не найден"); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemAndBookingDtoTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemAndBookingDtoTest.java new file mode 100644 index 0000000..5ade920 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/ItemAndBookingDtoTest.java @@ -0,0 +1,144 @@ +package ru.practicum.shareit.item; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.shareit.ShareItApp; +import ru.practicum.shareit.booking.dto.SimplifiedBookingDto; +import ru.practicum.shareit.item.comment.dto.CommentResponseDto; +import ru.practicum.shareit.item.dto.ItemBookingDto; +import ru.practicum.shareit.item.dto.ItemDto; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@ContextConfiguration(classes = ShareItApp.class) +@JsonTest +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class ItemAndBookingDtoTest { + private final JacksonTester jsonItemDto; + private final JacksonTester jsonCommentResponseDto; + private final JacksonTester jsonSimplifiedBookingDto; + private final JacksonTester jsonItemBookingDto; + + + @Test + void shouldReturnItemDto() throws Exception { + ItemDto itemDto = new ItemDto( + 1L, + "Canon 500d", + "Полупрофессиональная модель фотоаппарата", + true, + 5L + ); + + JsonContent result = jsonItemDto.write(itemDto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Canon 500d"); + assertThat(result).extractingJsonPathStringValue("$.description").isEqualTo("Полупрофессиональная модель фотоаппарата"); + assertThat(result).extractingJsonPathBooleanValue("$.available").isEqualTo(true); + assertThat(result).extractingJsonPathNumberValue("$.requestId").isEqualTo(5); + } + + @Test + void shouldReturnItemBookingDto() throws Exception { + + CommentResponseDto commentResponseDto = new CommentResponseDto( + 10L, + "Все понравилось. Рекомендую!", + "Александра Иванова", + LocalDateTime.of(2025, 7, 19, 17, 58, 0), + 2L + ); + + SimplifiedBookingDto simplifiedBookingDto = new SimplifiedBookingDto( + 1L, + 1L + ); + + SimplifiedBookingDto simplifiedBookingDto2 = new SimplifiedBookingDto( + 3L, + 2L + ); + + ItemBookingDto itemBookingDto = new ItemBookingDto( + 2L, + "Canon 500d", + "Полупрофессиональная модель фотоаппарата", + true, + simplifiedBookingDto, + simplifiedBookingDto2, + List.of(commentResponseDto) + ); + + JsonContent resultCommentResponseDto = jsonCommentResponseDto.write(commentResponseDto); + JsonContent resultItemDtoSimplifiedBookingDto = jsonSimplifiedBookingDto.write(simplifiedBookingDto); + JsonContent resultItemDtoSimplifiedBookingDto2 = jsonSimplifiedBookingDto.write(simplifiedBookingDto2); + JsonContent resultItemBookingDto = jsonItemBookingDto.write(itemBookingDto); + + assertThat(resultItemBookingDto).extractingJsonPathNumberValue("$.id").isEqualTo(2); + assertThat(resultItemBookingDto).extractingJsonPathStringValue("$.name").isEqualTo("Canon 500d"); + assertThat(resultItemBookingDto).extractingJsonPathStringValue("$.description").isEqualTo("Полупрофессиональная модель фотоаппарата"); + assertThat(resultItemBookingDto).extractingJsonPathBooleanValue("$.available").isEqualTo(true); + assertThat(resultItemDtoSimplifiedBookingDto).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(resultItemDtoSimplifiedBookingDto).extractingJsonPathNumberValue("$.bookerId").isEqualTo(1); + assertThat(resultItemDtoSimplifiedBookingDto2).extractingJsonPathNumberValue("$.id").isEqualTo(3); + assertThat(resultItemDtoSimplifiedBookingDto2).extractingJsonPathNumberValue("$.bookerId").isEqualTo(2); + assertThat(resultCommentResponseDto).extractingJsonPathNumberValue("$.id").isEqualTo(10); + assertThat(resultCommentResponseDto).extractingJsonPathStringValue("$.text").isEqualTo("Все понравилось. Рекомендую!"); + assertThat(resultCommentResponseDto).extractingJsonPathStringValue("$.authorName").isEqualTo("Александра Иванова"); + assertThat(resultCommentResponseDto).extractingJsonPathStringValue("$.created").isEqualTo("2025-07-19T17:58:00"); + assertThat(resultCommentResponseDto).extractingJsonPathNumberValue("$.itemId").isEqualTo(2); + } + + @Test + void shouldDeserializeItemBookingDtoFromJson() throws Exception { + String json = "{ \"id\": 2, \"name\": \"Canon 500d\", \"description\": \"Полупрофессиональная модель фотоаппарата\", \"available\": true, \"lastBooking\": { \"id\": 1, \"bookerId\": 1 }, \"nextBooking\": { \"id\": 3, \"bookerId\": 2 }, \"comments\": [ { \"id\": 10, \"text\": \"Все понравилось. Рекомендую!\", \"authorName\": \"Александра Иванова\", \"created\": \"2025-07-19T17:58:00\", \"itemId\": 2 } ] }"; + + ItemBookingDto result = jsonItemBookingDto.parse(json).getObject(); + + assertThat(result.getId()).isEqualTo(2L); + assertThat(result.getName()).isEqualTo("Canon 500d"); + assertThat(result.getDescription()).isEqualTo("Полупрофессиональная модель фотоаппарата"); + assertThat(result.getAvailable()).isTrue(); + + assertThat(result.getLastBooking().getId()).isEqualTo(1L); + assertThat(result.getLastBooking().getBookerId()).isEqualTo(1L); + assertThat(result.getNextBooking().getId()).isEqualTo(3L); + assertThat(result.getNextBooking().getBookerId()).isEqualTo(2L); + + assertThat(result.getComments()).hasSize(1); + assertThat(result.getComments().get(0).getText()).isEqualTo("Все понравилось. Рекомендую!"); + } + + @Test + void shouldSerializeItemBookingDtoWithNullFields() throws Exception { + ItemBookingDto itemBookingDto = new ItemBookingDto( + 2L, + "Canon 500d", + "Полупрофессиональная модель фотоаппарата", + true, + null, + null, + null + ); + + JsonContent result = jsonItemBookingDto.write(itemBookingDto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(2); + assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Canon 500d"); + assertThat(result).extractingJsonPathStringValue("$.description").isEqualTo("Полупрофессиональная модель фотоаппарата"); + assertThat(result).extractingJsonPathBooleanValue("$.available").isEqualTo(true); + + assertThat(result).hasJsonPath("$.lastBooking"); + assertThat(result).hasJsonPath("$.nextBooking"); + assertThat(result).hasJsonPath("$.comments"); + } +} 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..f563227 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/ItemControllerTest.java @@ -0,0 +1,250 @@ +package ru.practicum.shareit.item; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +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.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.shareit.error.ErrorHandler; +import ru.practicum.shareit.item.comment.dto.CommentRequestDto; +import ru.practicum.shareit.item.comment.dto.CommentResponseDto; +import ru.practicum.shareit.item.controller.ItemController; +import ru.practicum.shareit.item.dto.ItemBookingDto; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.service.ItemService; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.List; + +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +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.patch; +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; + +@ExtendWith(MockitoExtension.class) +public class ItemControllerTest { + @Mock + private ItemService itemService; + @InjectMocks + private ItemController itemController; + private final ObjectMapper mapper = new ObjectMapper(); + private MockMvc mvc; + private ItemDto itemDto; + private ItemDto updatedItemDto; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + + mvc = MockMvcBuilders + .standaloneSetup(itemController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + itemDto = new ItemDto( + 1L, + "Karcher WD 2 Plus", + "Строительный пылесос", + true, + 2L + ); + + updatedItemDto = new ItemDto( + 1L, + "Karcher WD 2 Plus", + "Промышленный пылесос", + true, + 2L + ); + } + + @Test + void mustCreateItem() throws Exception { + when(itemService.createItem(eq(1L), any())) + .thenReturn(itemDto); + + mvc.perform(post("/items") + .header("X-Sharer-User-Id", 1L) + .content(mapper.writeValueAsString(itemDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.name", is("Karcher WD 2 Plus"))) + .andExpect(jsonPath("$.description", is("Строительный пылесос"))) + .andExpect(jsonPath("$.available", is(true))) + .andExpect(jsonPath("$.requestId", is(2))); + } + + @Test + void mustUpdateItem() throws Exception { + ItemDto patchDto = new ItemDto( + null, + null, + "Промышленный пылесос", + null, + null + ); + + when(itemService.updateItem(eq(1L), eq(1L), any())) + .thenReturn(updatedItemDto); + + mvc.perform(patch("/items/1") + .header("X-Sharer-User-Id", 1L) + .content(mapper.writeValueAsString(patchDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.name", is("Karcher WD 2 Plus"))) + .andExpect(jsonPath("$.description", is("Промышленный пылесос"))) + .andExpect(jsonPath("$.available", is(true))) + .andExpect(jsonPath("$.requestId", is(2))); + } + + @Test + void mustReturnItemById() throws Exception { + ItemBookingDto itemBookingDto = new ItemBookingDto( + 1L, + "Karcher WD 2 Plus", + "Строительный пылесос", + true, + null, + null, + List.of() + ); + + when(itemService.findItemById(eq(1L), eq(1L))) + .thenReturn(itemBookingDto); + + mvc.perform(get("/items/1") + .header("X-Sharer-User-Id", 1L) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.name", is("Karcher WD 2 Plus"))) + .andExpect(jsonPath("$.description", is("Строительный пылесос"))) + .andExpect(jsonPath("$.available", is(true))) + .andExpect(jsonPath("$.lastBooking").doesNotExist()) + .andExpect(jsonPath("$.nextBooking").doesNotExist()) + .andExpect(jsonPath("$.comments", is(empty()))); + } + + @Test + void mustReturnUserItems() throws Exception { + List items = List.of( + new ItemBookingDto( + 1L, + "Karcher WD 2 Plus", + "Строительный пылесос", + true, + null, + null, + List.of() + ), + new ItemBookingDto( + 2L, + "Bosch GSB 13 RE", + "Ударная дрель", + true, + null, + null, + List.of() + ) + ); + + when(itemService.findUserItems(1L)) + .thenReturn(items); + + mvc.perform(get("/items") + .header("X-Sharer-User-Id", 1L) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.size()", is(2))) + .andExpect(jsonPath("$[0].id", is(1))) + .andExpect(jsonPath("$[0].name", is("Karcher WD 2 Plus"))) + .andExpect(jsonPath("$[1].id", is(2))) + .andExpect(jsonPath("$[1].name", is("Bosch GSB 13 RE"))); + } + + @Test + void mustSearchItemsByText() throws Exception { + List searchResults = List.of( + new ItemDto( + 1L, + "Bosch GSB 13 RE", + "Ударная дрель", + true, + null + ), + new ItemDto( + 2L, + "Makita HP1631K", + "Дрель с ударным механизмом", + true, + null + ) + ); + + when(itemService.searchItem("дрель")).thenReturn(searchResults); + + mvc.perform(get("/items/search") + .param("text", "дрель") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.size()", is(2))) + .andExpect(jsonPath("$[0].id", is(1))) + .andExpect(jsonPath("$[0].name", is("Bosch GSB 13 RE"))) + .andExpect(jsonPath("$[1].id", is(2))) + .andExpect(jsonPath("$[1].name", is("Makita HP1631K"))); + } + + @Test + void mustReturnComment() throws Exception { + Long userId = 1L; + Long itemId = 1L; + + CommentRequestDto requestDto = new CommentRequestDto("Все хорошо!"); + + CommentResponseDto responseDto = new CommentResponseDto( + 1L, + "Все хорошо!", + "Макс Иванов", + LocalDateTime.of(2025, 7, 20, 11, 30), + 1L + ); + + when(itemService.createComment(eq(userId), eq(itemId), any())) + .thenReturn(responseDto); + + mvc.perform(post("/items/{itemId}/comment", itemId) + .header("X-Sharer-User-Id", userId) + .content(mapper.writeValueAsString(requestDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.text", is("Все хорошо!"))) + .andExpect(jsonPath("$.authorName", is("Макс Иванов"))) + .andExpect(jsonPath("$.created[0]", is(2025))) + .andExpect(jsonPath("$.created[1]", is(7))) + .andExpect(jsonPath("$.created[2]", is(20))) + .andExpect(jsonPath("$.created[3]", is(11))) + .andExpect(jsonPath("$.created[4]", is(30))); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemMapperTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemMapperTest.java new file mode 100644 index 0000000..2e944ef --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/ItemMapperTest.java @@ -0,0 +1,102 @@ +package ru.practicum.shareit.item; + +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.mapper.ItemMapper; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.model.User; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class ItemMapperTest { + @Test + void mustMapItemToItemDtoWithRequest() { + ItemRequest itemRequest = new ItemRequest(); + itemRequest.setId(42L); + + Item item = new Item( + 1L, + "Canon 500d", + "Зеркальный фотоаппарат", + true, new User(), itemRequest + ); + + ItemDto itemDto = ItemMapper.toItemDto(item); + + assertThat(itemDto.getId()).isEqualTo(1L); + assertThat(itemDto.getName()).isEqualTo("Canon 500d"); + assertThat(itemDto.getDescription()).isEqualTo("Зеркальный фотоаппарат"); + assertThat(itemDto.getAvailable()).isTrue(); + assertThat(itemDto.getRequestId()).isEqualTo(42L); + } + + @Test + void mustMapItemToItemDtoWithNullRequest() { + Item item = new Item( + 1L, + "Canon 500d", + "Зеркальный фотоаппарат", + false, + new User(), + null + ); + + ItemDto itemDto = ItemMapper.toItemDto(item); + + assertThat(itemDto.getRequestId()).isNull(); + } + + @Test + void mustMapItemDtoToItemWithoutRequest() { + User user = new User( + 1L, + "Макс Иванов", + "Max@mail.com" + ); + + ItemDto itemDto = new ItemDto( + null, + "Canon 500d", + "Зеркальный фотоаппарат", + true, + null + ); + + Item item = ItemMapper.toItem(user, itemDto); + + assertThat(item.getId()).isNull(); + assertThat(item.getName()).isEqualTo("Canon 500d"); + assertThat(item.getDescription()).isEqualTo("Зеркальный фотоаппарат"); + assertThat(item.getAvailable()).isTrue(); + assertThat(item.getOwner()).isEqualTo(user); + assertThat(item.getItemRequest()).isNull(); + } + + @Test + void mustMapItemDtoToItemWithRequest() { + User user = new User( + 20L, + "Анна Иванова", + "Anna@mail.com" + ); + ItemRequest itemRequest = new ItemRequest(); + itemRequest.setId(100L); + + ItemDto itemDto = new ItemDto( + null, + "Canon 500d", + "Зеркальный фотоаппарат", + false, + 100L + ); + + Item item = ItemMapper.toItem(user, itemDto, itemRequest); + + assertThat(item.getName()).isEqualTo("Canon 500d"); + assertThat(item.getDescription()).isEqualTo("Зеркальный фотоаппарат"); + assertThat(item.getAvailable()).isFalse(); + assertThat(item.getOwner()).isEqualTo(user); + assertThat(item.getItemRequest()).isEqualTo(itemRequest); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemServiceTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemServiceTest.java new file mode 100644 index 0000000..e56c52d --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/ItemServiceTest.java @@ -0,0 +1,330 @@ +package ru.practicum.shareit.item; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.ShareItApp; +import ru.practicum.shareit.booking.model.Booking; +import ru.practicum.shareit.booking.status.RentalStatus; +import ru.practicum.shareit.item.comment.dto.CommentRequestDto; +import ru.practicum.shareit.item.comment.dto.CommentResponseDto; +import ru.practicum.shareit.item.comment.model.Comment; +import ru.practicum.shareit.item.dto.ItemBookingDto; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.service.ItemService; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@Transactional +@ActiveProfiles("test") +@SpringBootTest( + classes = ShareItApp.class, + webEnvironment = SpringBootTest.WebEnvironment.NONE) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class ItemServiceTest { + private final EntityManager em; + private final ItemService itemService; + + @Test + void mustReturnUserItems() { + User owner = new User(); + owner.setName("Владимир Петухов"); + owner.setEmail("Vladimir@mail.ru"); + em.persist(owner); + + User booker = new User(); + booker.setName("Петр Васильев"); + booker.setEmail("Petr@mail.ru"); + em.persist(booker); + + Item item = new Item(); + item.setName("Canon 500d"); + item.setDescription("Зеркальный фотоаппарат"); + item.setAvailable(true); + item.setOwner(owner); + em.persist(item); + + Booking pastBooking = new Booking(); + pastBooking.setStart(LocalDateTime.now().minusDays(4)); + pastBooking.setEnd(LocalDateTime.now().minusDays(1)); + pastBooking.setItem(item); + pastBooking.setBooker(booker); + pastBooking.setStatus(RentalStatus.APPROVED); + em.persist(pastBooking); + + Booking futureBooking = new Booking(); + futureBooking.setStart(LocalDateTime.now().plusDays(1)); + futureBooking.setEnd(LocalDateTime.now().plusDays(4)); + futureBooking.setItem(item); + futureBooking.setBooker(booker); + futureBooking.setStatus(RentalStatus.APPROVED); + em.persist(futureBooking); + + Comment comment = new Comment(); + comment.setText("Все понравилось. Рекомендую!"); + comment.setItem(item); + comment.setAuthor(booker); + comment.setCreated(LocalDateTime.now().minusHours(2)); + em.persist(comment); + + em.flush(); + em.clear(); + + List result = itemService.findUserItems(owner.getId()); + + assertThat(result).hasSize(1); + + ItemBookingDto itemBookingDto = result.get(0); + assertThat(itemBookingDto.getName()).isEqualTo("Canon 500d"); + assertThat(itemBookingDto.getDescription()).isEqualTo("Зеркальный фотоаппарат"); + assertThat(itemBookingDto.getAvailable()).isEqualTo(true); + assertThat(itemBookingDto.getLastBooking()).isNotNull(); + assertThat(itemBookingDto.getNextBooking()).isNotNull(); + assertThat(itemBookingDto.getComments()).hasSize(1); + assertThat(itemBookingDto.getComments().get(0).getText()).isEqualTo("Все понравилось. Рекомендую!"); + } + + @Test + void mustCreateItemWithRequestId() { + User user = new User(); + user.setName("Андрей Алексеев"); + user.setEmail("Andrey@mail.ru"); + em.persist(user); + + ItemRequest itemRequest = new ItemRequest(); + itemRequest.setDescription("Нужен строительный пылесос"); + itemRequest.setRequestor(user); + itemRequest.setCreated(LocalDateTime.now()); + em.persist(itemRequest); + em.flush(); + + ItemDto itemDto = new ItemDto(null, "Karcher WD 2 Plus", "Строительный пылесос", true, itemRequest.getId()); + + ItemDto result = itemService.createItem(user.getId(), itemDto); + + assertThat(result.getId()).isNotNull(); + assertThat(result.getRequestId()).isEqualTo(itemRequest.getId()); + } + + @Test + void mustCreateItemWithoutRequestId() { + User user = new User(); + user.setName("Макс Иванов"); + user.setEmail("Max@mail.ru"); + em.persist(user); + em.flush(); + + ItemDto itemDto = new ItemDto(null, "Dykemann", "Удобное кресло", true, null); + + ItemDto result = itemService.createItem(user.getId(), itemDto); + + assertThat(result.getId()).isNotNull(); + assertThat(result.getRequestId()).isNull(); + } + + @Test + void mustUpdateItemFields() { + User user = new User(); + user.setName("Макс Иванов"); + user.setEmail("Max@mail.ru"); + em.persist(user); + + Item item = new Item(); + item.setName("Стол для учебы"); + item.setDescription("Старый"); + item.setAvailable(true); + item.setOwner(user); + em.persist(item); + em.flush(); + + ItemDto updateItemDto = new ItemDto(null, "Новый стол для учебы", "Современный", false, null); + + ItemDto result = itemService.updateItem(item.getId(), user.getId(), updateItemDto); + + assertThat(result.getName()).isEqualTo("Новый стол для учебы"); + assertThat(result.getDescription()).isEqualTo("Современный"); + assertThat(result.getAvailable()).isFalse(); + } + + @Test + void mustReturnItemWithBookingsForOwner() { + User owner = new User(); + owner.setName("Макс Иванов"); + owner.setEmail("Max@mail.ru"); + em.persist(owner); + + User booker = new User(); + booker.setName("Андрей Алексеев"); + booker.setEmail("Andrey@mail.ru"); + em.persist(booker); + + Item item = new Item(); + item.setName("Yamaha PACIFICA 012 BL"); + item.setDescription("Электрогитара"); + item.setAvailable(true); + item.setOwner(owner); + em.persist(item); + + Booking pastBooking = new Booking(); + pastBooking.setItem(item); + pastBooking.setBooker(booker); + pastBooking.setStart(LocalDateTime.now().minusDays(5)); + pastBooking.setEnd(LocalDateTime.now().minusDays(2)); + pastBooking.setStatus(RentalStatus.APPROVED); + em.persist(pastBooking); + + Booking futureBooking = new Booking(); + futureBooking.setItem(item); + futureBooking.setBooker(booker); + futureBooking.setStart(LocalDateTime.now().plusDays(1)); + futureBooking.setEnd(LocalDateTime.now().plusDays(3)); + futureBooking.setStatus(RentalStatus.APPROVED); + em.persist(futureBooking); + + Comment comment = new Comment(); + comment.setText("Отличный инструмент!"); + comment.setAuthor(booker); + comment.setItem(item); + comment.setCreated(LocalDateTime.now().minusHours(1)); + em.persist(comment); + + em.flush(); + em.clear(); + + ItemBookingDto result = itemService.findItemById(owner.getId(), item.getId()); + + assertThat(result.getLastBooking()).isNotNull(); + assertThat(result.getNextBooking()).isNotNull(); + assertThat(result.getComments()).hasSize(1); + } + + @Test + void mustReturnItemWithoutBookingsForOtherUser() { + User owner = new User(); + owner.setName("Макс Иванов"); + owner.setEmail("Max@mail.ru"); + em.persist(owner); + + User booker = new User(); + booker.setName("Андрей Алексеев"); + booker.setEmail("Andrey@mail.ru"); + em.persist(booker); + + Item item = new Item(); + item.setName("Судьба человека"); + item.setDescription("Классическое произведение"); + item.setAvailable(true); + item.setOwner(owner); + em.persist(item); + + em.flush(); + + ItemBookingDto result = itemService.findItemById(booker.getId(), item.getId()); + + assertThat(result.getLastBooking()).isNull(); + assertThat(result.getNextBooking()).isNull(); + } + + @Test + void mustSearchOnlyAvailableItems() { + User user = new User(); + user.setName("Макс Иванов"); + user.setEmail("Max@mail.ru"); + em.persist(user); + + Item available = new Item(); + available.setName("Телевизор Samsung"); + available.setDescription("Smart TV"); + available.setAvailable(true); + available.setOwner(user); + em.persist(available); + + Item unavailable = new Item(); + unavailable.setName("Телевизор Rolsen"); + unavailable.setDescription("Аналоговый"); + unavailable.setAvailable(false); + unavailable.setOwner(user); + em.persist(unavailable); + + em.flush(); + + List result = itemService.searchItem("Телевизор"); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getAvailable()).isTrue(); + } + + @Test + void mustCreateCommentAfterBooking() { + User owner = new User(); + owner.setName("Макс Иванов"); + owner.setEmail("Max@mail.ru"); + em.persist(owner); + + User booker = new User(); + booker.setName("Андрей Алексеев"); + booker.setEmail("Andrey@mail.ru"); + em.persist(booker); + + Item item = new Item(); + item.setName("Atom"); + item.setDescription("Велосипед горный"); + item.setAvailable(true); + item.setOwner(owner); + em.persist(item); + + Booking booking = new Booking(); + booking.setItem(item); + booking.setBooker(booker); + booking.setStart(LocalDateTime.now().minusDays(5)); + booking.setEnd(LocalDateTime.now().minusDays(2)); + booking.setStatus(RentalStatus.APPROVED); + em.persist(booking); + + em.flush(); + + CommentRequestDto dto = new CommentRequestDto("Крутой велосипед! Спасибо!"); + CommentResponseDto result = itemService.createComment(booker.getId(), item.getId(), dto); + + assertThat(result.getText()).isEqualTo("Крутой велосипед! Спасибо!"); + } + + @Test + void mustThrowWhenCommentWithoutBooking() { + User owner = new User(); + owner.setName("Макс Иванов"); + owner.setEmail("Max@mail.ru"); + em.persist(owner); + + User booker = new User(); + booker.setName("Андрей Алексеев"); + booker.setEmail("Andrey@mail.ru"); + em.persist(booker); + + Item item = new Item(); + item.setName("Canon 500d"); + item.setDescription("Зеркальный фотоаппарат"); + item.setAvailable(true); + item.setOwner(owner); + em.persist(item); + + em.flush(); + + CommentRequestDto dto = new CommentRequestDto("Хочу оставить отзыв"); + + assertThatThrownBy(() -> itemService.createComment(booker.getId(), item.getId(), dto)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Пользователь может оставить комментарий после аренды"); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/item/comment/CommentMapperTest.java b/server/src/test/java/ru/practicum/shareit/item/comment/CommentMapperTest.java new file mode 100644 index 0000000..88fe270 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/comment/CommentMapperTest.java @@ -0,0 +1,44 @@ +package ru.practicum.shareit.item.comment; + +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.item.comment.dto.CommentRequestDto; +import ru.practicum.shareit.item.comment.dto.mapper.CommentMapper; +import ru.practicum.shareit.item.comment.model.Comment; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.user.model.User; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class CommentMapperTest { + @Test + void mustMapCommentRequestDtoToComment() { + + CommentRequestDto requestDto = new CommentRequestDto("Отличная вещь!"); + User author = new User( + 1L, + "Макс Иванов", + "Max@mail.ru" + ); + + Item item = new Item( + 2L, + "Canon 500d", + "Зеркальный фотоаппарат", + true, + author, + null + ); + + LocalDateTime before = LocalDateTime.now().minusSeconds(1); + Comment comment = CommentMapper.toComment(requestDto, author, item); + LocalDateTime after = LocalDateTime.now().plusSeconds(1); + + assertThat(comment.getId()).isNull(); + assertThat(comment.getText()).isEqualTo("Отличная вещь!"); + assertThat(comment.getAuthor()).isEqualTo(author); + assertThat(comment.getItem()).isEqualTo(item); + assertThat(comment.getCreated()).isAfterOrEqualTo(before).isBeforeOrEqualTo(after); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/item/comment/CommentRequestAndResponseDtoTest.java b/server/src/test/java/ru/practicum/shareit/item/comment/CommentRequestAndResponseDtoTest.java new file mode 100644 index 0000000..e7e03ce --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/comment/CommentRequestAndResponseDtoTest.java @@ -0,0 +1,80 @@ +package ru.practicum.shareit.item.comment; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.shareit.ShareItApp; +import ru.practicum.shareit.item.comment.dto.CommentRequestDto; +import ru.practicum.shareit.item.comment.dto.CommentResponseDto; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@ContextConfiguration(classes = ShareItApp.class) +@JsonTest +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class CommentRequestAndResponseDtoTest { + private final JacksonTester jsonCommentRequestDto; + private final JacksonTester jsonCommentResponseDto; + + @Test + void shouldReturnCommentRequestDto() throws Exception { + CommentRequestDto commentRequestDto = new CommentRequestDto( + "Все понравилось. Рекомендую!" + ); + + JsonContent resultCommentRequestDto = jsonCommentRequestDto.write(commentRequestDto); + + assertThat(resultCommentRequestDto).extractingJsonPathStringValue("$.text").isEqualTo("Все понравилось. Рекомендую!"); + } + + @Test + void shouldReturnCommentResponseDto() throws Exception { + CommentResponseDto commentResponseDto = new CommentResponseDto( + 10L, + "Все понравилось. Рекомендую!", + "Александра Иванова", + LocalDateTime.of(2025, 7, 19, 17, 58, 0), + 2L + ); + + JsonContent resultCommentResponseDto = jsonCommentResponseDto.write(commentResponseDto); + + assertThat(resultCommentResponseDto).extractingJsonPathNumberValue("$.id").isEqualTo(10); + assertThat(resultCommentResponseDto).extractingJsonPathStringValue("$.text").isEqualTo("Все понравилось. Рекомендую!"); + assertThat(resultCommentResponseDto).extractingJsonPathStringValue("$.authorName").isEqualTo("Александра Иванова"); + assertThat(resultCommentResponseDto).extractingJsonPathStringValue("$.created").isEqualTo("2025-07-19T17:58:00"); + assertThat(resultCommentResponseDto).extractingJsonPathNumberValue("$.itemId").isEqualTo(2); + } + + @Test + void shouldReturnDeserializeCommentRequestDtoFromJson() throws Exception { + String json = "{ \"text\": \"Все понравилось. Рекомендую!\" }"; + + CommentRequestDto dto = jsonCommentRequestDto.parse(json).getObject(); + + assertThat(dto.getText()).isEqualTo("Все понравилось. Рекомендую!"); + } + + @Test + void shouldReturnSerializeCommentResponseDtoWithNullFields() throws Exception { + CommentResponseDto commentResponseDto = new CommentResponseDto( + 10L, + "Все понравилось. Рекомендую!", + null, + null, + 2L + ); + + JsonContent result = jsonCommentResponseDto.write(commentResponseDto); + + assertThat(result).hasJsonPath("$.authorName"); + assertThat(result).extractingJsonPathStringValue("$.authorName").isNull(); + assertThat(result).extractingJsonPathStringValue("$.created").isNull(); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/request/ItemReqRequestAndResponseDtoTest.java b/server/src/test/java/ru/practicum/shareit/request/ItemReqRequestAndResponseDtoTest.java new file mode 100644 index 0000000..5f58d5b --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/ItemReqRequestAndResponseDtoTest.java @@ -0,0 +1,105 @@ +package ru.practicum.shareit.request; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.shareit.ShareItApp; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.request.dto.ItemReqRequestDto; +import ru.practicum.shareit.request.dto.ItemReqResponseDto; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@ContextConfiguration(classes = ShareItApp.class) +@JsonTest +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class ItemReqRequestAndResponseDtoTest { + private final JacksonTester json; + private final JacksonTester jsonItemResponseDto; + private final JacksonTester jsonItemDto; + + @Test + void shouldReturnItemReqRequestDto() throws Exception { + ItemReqRequestDto itemReqRequestDto = new ItemReqRequestDto( + "Ищу строительный пылесос", + 1L + ); + + JsonContent result = json.write(itemReqRequestDto); + + assertThat(result).extractingJsonPathStringValue("$.description").isEqualTo("Ищу строительный пылесос"); + assertThat(result).extractingJsonPathNumberValue("$.requestorId").isEqualTo(1); + } + + @Test + void shouldReturnItemReqResponseDto() throws Exception { + ItemReqResponseDto itemReqResponseDto = new ItemReqResponseDto( + 2L, + "нужен фотоаппарат", + 3L, + LocalDateTime.of(2025, 7, 16, 18, 30, 0), + List.of() + ); + + JsonContent result = jsonItemResponseDto.write(itemReqResponseDto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(2); + assertThat(result).extractingJsonPathStringValue("$.description").isEqualTo("нужен фотоаппарат"); + assertThat(result).extractingJsonPathNumberValue("$.userId").isEqualTo(3); + assertThat(result).extractingJsonPathStringValue("$.created").isEqualTo("2025-07-16T18:30:00"); + assertThat(result).extractingJsonPathArrayValue("$.items").isEmpty(); + } + + @Test + void shouldReturnItemReqResponseDtoAndItemDto() throws Exception { + ItemDto itemDto = new ItemDto( + 5L, + "Canon 500d", + "Зеркальный фотоаппарат", + true, + 10L + ); + + ItemReqResponseDto itemReqResponseDto = new ItemReqResponseDto( + 2L, + "нужен фотоаппарат", + 3L, + LocalDateTime.of(2025, 7, 16, 18, 30, 0), + List.of(itemDto) + ); + + JsonContent result = jsonItemResponseDto.write(itemReqResponseDto); + JsonContent resultItemDto = jsonItemDto.write(itemDto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(2); + assertThat(result).extractingJsonPathStringValue("$.description").isEqualTo("нужен фотоаппарат"); + assertThat(result).extractingJsonPathNumberValue("$.userId").isEqualTo(3); + assertThat(result).extractingJsonPathStringValue("$.created").isEqualTo("2025-07-16T18:30:00"); + assertThat(resultItemDto).extractingJsonPathNumberValue("$.id").isEqualTo(5); + assertThat(resultItemDto).extractingJsonPathStringValue("$.name").isEqualTo("Canon 500d"); + assertThat(resultItemDto).extractingJsonPathStringValue("$.description").isEqualTo("Зеркальный фотоаппарат"); + assertThat(resultItemDto).extractingJsonPathBooleanValue("$.available").isEqualTo(true); + assertThat(resultItemDto).extractingJsonPathNumberValue("$.requestId").isEqualTo(10); + } + + @Test + void shouldDeserializeItemReqResponseDtoFromJson() throws Exception { + String json = "{ \"id\": 2, \"description\": \"нужен фотоаппарат\", \"userId\": 3, \"created\": \"2025-07-16T18:30:00\", \"items\": [] }"; + + ItemReqResponseDto result = jsonItemResponseDto.parse(json).getObject(); + + assertThat(result.getId()).isEqualTo(2L); + assertThat(result.getDescription()).isEqualTo("нужен фотоаппарат"); + assertThat(result.getUserId()).isEqualTo(3L); + assertThat(result.getCreated()).isEqualTo(LocalDateTime.of(2025, 7, 16, 18, 30, 0)); + assertThat(result.getItems()).isEmpty(); + } +} + 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..b79dbd7 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/ItemRequestControllerTest.java @@ -0,0 +1,197 @@ +package ru.practicum.shareit.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +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.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.shareit.error.ErrorHandler; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.request.controller.ItemRequestController; +import ru.practicum.shareit.request.dto.ItemReqRequestDto; +import ru.practicum.shareit.request.dto.ItemReqResponseDto; +import ru.practicum.shareit.request.service.ItemRequestService; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +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; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +@ExtendWith(MockitoExtension.class) +public class ItemRequestControllerTest { + @Mock + private ItemRequestService itemRequestService; + @InjectMocks + private ItemRequestController itemRequestController; + private final ObjectMapper mapper = new ObjectMapper(); + private MockMvc mvc; + private ItemReqRequestDto itemReqRequestDto; + private ItemDto itemDto; + private ItemReqResponseDto itemReqResponseDto; + private ItemReqResponseDto itemReqResponseDto2; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + + mvc = MockMvcBuilders + .standaloneSetup(itemRequestController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + itemReqRequestDto = new ItemReqRequestDto( + "Ищу строительный пылесос", + 1L + ); + + itemDto = new ItemDto( + 1L, + "Karcher WD 2 Plus", + "Строительный пылесос", + true, + 2L + ); + + itemReqResponseDto = new ItemReqResponseDto( + 1L, + "Ищу строительный пылесос", + 1L, + LocalDateTime.of(2025, 7, 16, 19, 40, 0), + List.of() + ); + + itemReqResponseDto2 = new ItemReqResponseDto( + 1L, + "Ищу строительный пылесос", + 1L, + LocalDateTime.of(2025, 7, 16, 19, 40, 0), + List.of(itemDto) + ); + } + + @Test + void mustCreateItemRequest() throws Exception { + when(itemRequestService.createItemRequest(eq(1L), any())) + .thenReturn(itemReqResponseDto); + + mvc.perform(post("/requests") + .header("X-Sharer-User-Id", 1L) + .content(mapper.writeValueAsString(itemReqRequestDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.description", is(itemReqRequestDto.getDescription()))) + .andExpect(jsonPath("$.userId", is(itemReqRequestDto.getRequestorId().intValue()))) + .andExpect(jsonPath("$.created[0]", is(2025))) + .andExpect(jsonPath("$.created[1]", is(7))) + .andExpect(jsonPath("$.created[2]", is(16))) + .andExpect(jsonPath("$.created[3]", is(19))) + .andExpect(jsonPath("$.created[4]", is(40))) + .andExpect(jsonPath("$.items").isArray()); + } + + @Test + void mustFindListOfYourQueriesWithAnswers() throws Exception { + when(itemRequestService.getRequests(1L)) + .thenReturn(List.of(itemReqResponseDto2)); + + mvc.perform(get("/requests") + .header("X-Sharer-User-Id", 1) + .characterEncoding(StandardCharsets.UTF_8) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id", is(1))) + .andExpect(jsonPath("$[0].description", is("Ищу строительный пылесос"))) + .andExpect(jsonPath("$[0].userId", is(1))) + .andExpect(jsonPath("$[0].created[0]", is(2025))) + .andExpect(jsonPath("$[0].created[1]", is(7))) + .andExpect(jsonPath("$[0].created[2]", is(16))) + .andExpect(jsonPath("$[0].created[3]", is(19))) + .andExpect(jsonPath("$[0].created[4]", is(40))) + .andExpect(jsonPath("$[0].items").isArray()) + .andExpect(jsonPath("$[0].items[0].id", is(1))) + .andExpect(jsonPath("$[0].items[0].name", is("Karcher WD 2 Plus"))) + .andExpect(jsonPath("$[0].items[0].description", is("Строительный пылесос"))) + .andExpect(jsonPath("$[0].items[0].available", is(true))) + .andExpect(jsonPath("$[0].items[0].requestId", is(2))); + } + + @Test + void mustFindListOfRequestsOtherUsers() throws Exception { + when(itemRequestService.getRequestsOtherUsers(1L, 0, 10)) + .thenReturn(List.of(itemReqResponseDto2)); + + mvc.perform(get("/requests/all") + .header("X-Sharer-User-Id", 1) + .param("from", "0") + .param("size", "10") + .characterEncoding(StandardCharsets.UTF_8) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id", is(1))) + .andExpect(jsonPath("$[0].description", is("Ищу строительный пылесос"))) + .andExpect(jsonPath("$[0].userId", is(1))) + .andExpect(jsonPath("$[0].created[0]", is(2025))) + .andExpect(jsonPath("$[0].created[1]", is(7))) + .andExpect(jsonPath("$[0].created[2]", is(16))) + .andExpect(jsonPath("$[0].created[3]", is(19))) + .andExpect(jsonPath("$[0].created[4]", is(40))) + .andExpect(jsonPath("$[0].items").isArray()) + .andExpect(jsonPath("$[0].items[0].id", is(1))) + .andExpect(jsonPath("$[0].items[0].name", is("Karcher WD 2 Plus"))) + .andExpect(jsonPath("$[0].items[0].description", is("Строительный пылесос"))) + .andExpect(jsonPath("$[0].items[0].available", is(true))) + .andExpect(jsonPath("$[0].items[0].requestId", is(2))); + } + + @Test + void mustFindYourQueryWithAnswers() throws Exception { + when(itemRequestService.getRequestWithAnswers(1L, 1L)) + .thenReturn(itemReqResponseDto2); + + mvc.perform(get("/requests/1") + .header("X-Sharer-User-Id", 1) + .characterEncoding(StandardCharsets.UTF_8) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.description", is("Ищу строительный пылесос"))) + .andExpect(jsonPath("$.userId", is(1))) + .andExpect(jsonPath("$.created[0]", is(2025))) + .andExpect(jsonPath("$.created[1]", is(7))) + .andExpect(jsonPath("$.created[2]", is(16))) + .andExpect(jsonPath("$.created[3]", is(19))) + .andExpect(jsonPath("$.created[4]", is(40))) + .andExpect(jsonPath("$.items").isArray()) + .andExpect(jsonPath("$.items[0].id", is(1))) + .andExpect(jsonPath("$.items[0].name", is("Karcher WD 2 Plus"))); + } + + @Test + void mustReturn404IfItemRequestNotFound() throws Exception { + when(itemRequestService.getRequestWithAnswers(1L, 99L)) + .thenThrow(new NotFoundException("Запрос не найден")); + + mvc.perform(get("/requests/99") + .header("X-Sharer-User-Id", 1) + .characterEncoding(StandardCharsets.UTF_8) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceTest.java b/server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceTest.java new file mode 100644 index 0000000..4c53a50 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceTest.java @@ -0,0 +1,149 @@ +package ru.practicum.shareit.request; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.ShareItApp; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.request.dto.ItemReqRequestDto; +import ru.practicum.shareit.request.dto.ItemReqResponseDto; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.request.service.ItemRequestService; +import ru.practicum.shareit.user.model.User; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +@Transactional +@ActiveProfiles("test") +@SpringBootTest( + classes = ShareItApp.class, + webEnvironment = SpringBootTest.WebEnvironment.NONE) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class ItemRequestServiceTest { + private final EntityManager em; + private final ItemRequestService itemRequestService; + private Long userId; + + @BeforeEach + void setUp() { + User user = new User(); + user.setName("Егор"); + user.setEmail("egor@mail.ru"); + + em.persist(user); + em.flush(); + + userId = user.getId(); + } + + @Test + public void mustCreateItemRequest() { + ItemReqRequestDto itemReqRequestDto = new ItemReqRequestDto("Ищу строительный пылесос", userId); + + ItemReqResponseDto itemReqResponseDto = itemRequestService.createItemRequest(userId, itemReqRequestDto); + + assertThat(itemReqResponseDto.getId()).isNotNull(); + assertThat(itemReqResponseDto.getDescription()).isEqualTo(itemReqResponseDto.getDescription()); + assertThat(itemReqResponseDto.getUserId()).isEqualTo(userId); + assertThat(itemReqResponseDto.getCreated()).isNotNull(); + assertThat(itemReqResponseDto.getItems()).isEmpty(); + + + ItemRequest savedItemRequest = em.createQuery( + "SELECT ir FROM ItemRequest ir WHERE ir.id = :id", ItemRequest.class) + .setParameter("id", itemReqResponseDto.getId()) + .getSingleResult(); + + assertThat(savedItemRequest.getDescription()).isEqualTo("Ищу строительный пылесос"); + assertThat(savedItemRequest.getRequestor().getId()).isEqualTo(userId); + } + + @Test + void mustReturnListOfYourQueriesWithAnswers() { + ItemRequest itemRequest = new ItemRequest(); + itemRequest.setDescription("Нужен фотоаппарат"); + itemRequest.setRequestor(em.find(User.class, userId)); + em.persist(itemRequest); + em.flush(); + + List results = itemRequestService.getRequests(userId); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getDescription()).isEqualTo("Нужен фотоаппарат"); + assertThat(results.get(0).getItems()).isEmpty(); + } + + @Test + void mustReturnListOfRequestsFromOtherUsers() { + User requestor = new User(); + requestor.setName("Катя Петрова"); + requestor.setEmail("Katya@mail.ru"); + em.persist(requestor); + + ItemRequest itemRequest = new ItemRequest(); + itemRequest.setDescription("Нужен фотоаппарат"); + itemRequest.setRequestor(requestor); + em.persist(itemRequest); + em.flush(); + + List results = itemRequestService.getRequestsOtherUsers(userId, 0, 10); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getDescription()).isEqualTo("Нужен фотоаппарат"); + } + + @Test + void mustReturnSingleRequestWithAnswers() { + User requestor = new User(); + requestor.setName("Катя Петрова"); + requestor.setEmail("Katya@mail.ru"); + em.persist(requestor); + + ItemRequest itemRequest = new ItemRequest(); + itemRequest.setDescription("Ищу гитару"); + itemRequest.setRequestor(requestor); + em.persist(itemRequest); + + Item item = new Item(); + item.setName("Yamaha PACIFICA 012 BL"); + item.setDescription("Электрогитара"); + item.setAvailable(true); + item.setOwner(em.find(User.class, userId)); + item.setItemRequest(itemRequest); + em.persist(item); + + em.flush(); + + ItemReqResponseDto result = itemRequestService.getRequestWithAnswers(userId, itemRequest.getId()); + + assertThat(result.getId()).isEqualTo(itemRequest.getId()); + assertThat(result.getItems()).hasSize(1); + assertThat(result.getItems().get(0).getName()).isEqualTo("Yamaha PACIFICA 012 BL"); + } + + @Test + void mustThrowWhenUserNotFound() { + assertThatThrownBy(() -> itemRequestService.getRequests(999L)) + .isInstanceOf(ru.practicum.shareit.exception.NotFoundException.class); + } + + @Test + void mustThrowWhenRequestNotFound() { + assertThatThrownBy(() -> itemRequestService.getRequestWithAnswers(userId, 999L)) + .isInstanceOf(ru.practicum.shareit.exception.NotFoundException.class); + } + + @Test + void mustReturnEmptyListWhenNoRequests() { + var result = itemRequestService.getRequests(userId); + assertThat(result).isEmpty(); + } +} 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..92d3196 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserControllerTest.java @@ -0,0 +1,131 @@ +package ru.practicum.shareit.user; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +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.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.shareit.error.ErrorHandler; +import ru.practicum.shareit.user.controller.UserController; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.service.UserService; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.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; + +@ExtendWith(MockitoExtension.class) +public class UserControllerTest { + @Mock + private UserService userService; + @InjectMocks + private UserController userController; + private final ObjectMapper mapper = new ObjectMapper(); + private MockMvc mvc; + private UserDto userDto; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + + mvc = MockMvcBuilders + .standaloneSetup(userController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + userDto = new UserDto( + 1L, + "Макс Иванов", + "Max@mail.ru" + ); + } + + @Test + void mustCreateUser() throws Exception { + when(userService.createUser(any())) + .thenReturn(userDto); + + mvc.perform(post("/users") + .header("X-Sharer-User-Id", 1L) + .content(mapper.writeValueAsString(userDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.name", is("Макс Иванов"))) + .andExpect(jsonPath("$.email", is("Max@mail.ru"))); + } + + @Test + void mustReturnAllUsers() throws Exception { + List users = List.of( + new UserDto(1L, "Артем Егоров", "Egorov@mail.ru"), + new UserDto(2L, "Аня Иванова", "Anya@mail.ru") + ); + + when(userService.findAllUsers()) + .thenReturn(users); + mvc.perform(get("/users") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.size()").value(2)) + .andExpect(jsonPath("$[0].id", is(1))) + .andExpect(jsonPath("$[0].name", is("Артем Егоров"))) + .andExpect(jsonPath("$[0].email", is("Egorov@mail.ru"))) + .andExpect(jsonPath("$[1].id", is(2))) + .andExpect(jsonPath("$[1].name", is("Аня Иванова"))) + .andExpect(jsonPath("$[1].email", is("Anya@mail.ru"))); + } + + @Test + void mustReturnUserById() throws Exception { + when(userService.findUserById(1L)) + .thenReturn(userDto); + + mvc.perform(get("/users/1") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.name", is("Макс Иванов"))) + .andExpect(jsonPath("$.email", is("Max@mail.ru"))); + } + + @Test + void mustUpdateUser() throws Exception { + UserDto updatedUser = new UserDto(1L, "Макс Иванов", "MaxIv@mail.ru"); + when(userService.updateUser(eq(1L), any(UserDto.class))) + .thenReturn(updatedUser); + + mvc.perform(patch("/users/1") + .content(mapper.writeValueAsString(updatedUser)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.name", is("Макс Иванов"))) + .andExpect(jsonPath("$.email", is("MaxIv@mail.ru"))); + } + + @Test + void mustDeleteUser() throws Exception { + mvc.perform(delete("/users/1")) + .andExpect(status().isOk()); + + verify(userService).deleteUser(1L); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/user/UserDtoTest.java b/server/src/test/java/ru/practicum/shareit/user/UserDtoTest.java new file mode 100644 index 0000000..278d3eb --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserDtoTest.java @@ -0,0 +1,47 @@ +package ru.practicum.shareit.user; + +import lombok.RequiredArgsConstructor; +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.shareit.ShareItApp; +import ru.practicum.shareit.user.dto.UserDto; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@ContextConfiguration(classes = ShareItApp.class) +@JsonTest +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class UserDtoTest { + private final JacksonTester jsonUserDto; + + @Test + void shouldReturnUserDto() throws Exception { + UserDto userDto = new UserDto( + 1L, + "Макс Иванов", + "Max@mail.ru" + ); + + JsonContent result = jsonUserDto.write(userDto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Макс Иванов"); + assertThat(result).extractingJsonPathStringValue("$.email").isEqualTo("Max@mail.ru"); + } + + @Test + void shouldDeserializeUserDtoFromJson() throws Exception { + String json = "{ \"id\": 1, \"name\": \"Макс Иванов\", \"email\": \"Max@mail.ru\" }"; + + UserDto result = jsonUserDto.parse(json).getObject(); + + AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L); + AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Макс Иванов"); + AssertionsForClassTypes.assertThat(result.getEmail()).isEqualTo("Max@mail.ru"); + } +} 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..8058896 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserMapperTest.java @@ -0,0 +1,20 @@ +package ru.practicum.shareit.user; + +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.dto.mapper.UserMapper; +import ru.practicum.shareit.user.model.User; + +public class UserMapperTest { + @Test + void shouldMapUserDtoToUser() { + UserDto dto = new UserDto(1L, "Макс Иванов", "max@mail.ru"); + + User result = UserMapper.toUser(dto); + + AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L); + AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Макс Иванов"); + AssertionsForClassTypes.assertThat(result.getEmail()).isEqualTo("max@mail.ru"); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/user/UserServiceTest.java b/server/src/test/java/ru/practicum/shareit/user/UserServiceTest.java new file mode 100644 index 0000000..e8f1468 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserServiceTest.java @@ -0,0 +1,135 @@ +package ru.practicum.shareit.user; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.ShareItApp; +import ru.practicum.shareit.exception.InternalServerException; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.service.UserService; + +import static org.assertj.core.api.Assertions.assertThat; + +@Transactional +@ActiveProfiles("test") +@SpringBootTest( + classes = ShareItApp.class, + webEnvironment = SpringBootTest.WebEnvironment.NONE) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class UserServiceTest { + private final EntityManager em; + private final UserService userService; + private Long userId; + + @BeforeEach + void setUp() { + User user = new User(); + user.setName("Макс Иванов"); + user.setEmail("Max@mail.ru"); + + em.persist(user); + em.flush(); + + userId = user.getId(); + } + + @Test + void mustReturnUserById() { + UserDto itemResponseDto = userService.findUserById(userId); + + assertThat(itemResponseDto.getId()).isNotNull(); + assertThat(itemResponseDto.getId()).isEqualTo(userId); + assertThat(itemResponseDto.getName()).isEqualTo("Макс Иванов"); + assertThat(itemResponseDto.getEmail()).isEqualTo("Max@mail.ru"); + } + + + @Test + void shouldReturnException() { + Assertions.assertThrows(NotFoundException.class, () -> userService.findUserById(999999L)); + } + + @Test + void mustUpdateName() { + UserDto updateDto = new UserDto(); + updateDto.setName("Макс Алексеев"); + updateDto.setEmail(null); + + UserDto updated = userService.updateUser(userId, updateDto); + + assertThat(updated.getName()).isEqualTo("Макс Алексеев"); + assertThat(updated.getEmail()).isEqualTo("Max@mail.ru"); + } + + @Test + void mustUpdateEmail() { + UserDto updateDto = new UserDto(); + updateDto.setName(null); + updateDto.setEmail("newMax@mail.ru"); + + UserDto updated = userService.updateUser(userId, updateDto); + + assertThat(updated.getEmail()).isEqualTo("newMax@mail.ru"); + assertThat(updated.getName()).isEqualTo("Макс Иванов"); + } + + @Test + void emptyFieldsShouldIgnoredWhenUpdating() { + UserDto updateDto = new UserDto(); + updateDto.setName(" "); + updateDto.setEmail(" "); + + UserDto updated = userService.updateUser(userId, updateDto); + + assertThat(updated.getName()).isEqualTo("Макс Иванов"); + assertThat(updated.getEmail()).isEqualTo("Max@mail.ru"); + } + + @Test + void shouldSuccessfullyCreateTheUser() { + UserDto newUser = new UserDto(); + newUser.setName("Иван Петров"); + newUser.setEmail("ivan.petrov@mail.ru"); + + UserDto created = userService.createUser(newUser); + + assertThat(created.getId()).isNotNull(); + assertThat(created.getName()).isEqualTo("Иван Петров"); + assertThat(created.getEmail()).isEqualTo("ivan.petrov@mail.ru"); + } + + @Test + void exceptionMustBeThrown() { + UserDto newUser = new UserDto(); + newUser.setName("Макс Егоров"); + newUser.setEmail("Max@mail.ru"); + + Assertions.assertThrows(InternalServerException.class, () -> userService.createUser(newUser)); + } + + @Test + void deleteUserExistingIdSuccess() { + Assertions.assertDoesNotThrow(() -> userService.deleteUser(userId)); + } + + @Test + void shouldReturnExceptionOnDelete() { + Assertions.assertDoesNotThrow(() -> userService.deleteUser(999999L)); + } + + @Test + void shouldReturnAllUsers() { + var allUsers = userService.findAllUsers(); + + assertThat(allUsers).isNotEmpty(); + assertThat(allUsers.stream().anyMatch(u -> u.getId().equals(userId))).isTrue(); + } +} 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/resources/application.yaml b/src/main/resources/application.yaml deleted file mode 100644 index f5599d8..0000000 --- a/src/main/resources/application.yaml +++ /dev/null @@ -1,35 +0,0 @@ -server: - port: 8080 - -spring: - sql: - init: - mode: always - jpa: - show-sql: true - hibernate: - ddl-auto: none - properties: - hibernate: - format_sql: true - jdbc: - time_zone: UTC - lob: - non_contextual_creation: true - - datasource: - url: jdbc:postgresql://localhost:5438/shareitdb - username: dbuser - password: 12345 - driver-class-name: org.postgresql.Driver - -logging: - level: - org: - springframework: - orm: - jpa: INFO - jpa.JpaTransactionManager: DEBUG - transaction: - interceptor: TRACE - transaction.interceptor: TRACE \ No newline at end of file