diff --git a/common/pom.xml b/common/pom.xml new file mode 100644 index 0000000..eb95870 --- /dev/null +++ b/common/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + ru.practicum + shareit + 0.0.1-SNAPSHOT + + + common + 0.0.1-SNAPSHOT + jar + + + + org.springframework.boot + spring-boot-starter-validation + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + \ No newline at end of file diff --git a/common/src/main/java/ru/practicum/ShareItCommon.java b/common/src/main/java/ru/practicum/ShareItCommon.java new file mode 100644 index 0000000..09a46b0 --- /dev/null +++ b/common/src/main/java/ru/practicum/ShareItCommon.java @@ -0,0 +1,12 @@ +package ru.practicum; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ShareItCommon { + public static void main(String[] args) { + SpringApplication.run(ShareItCommon.class, args); + } + +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/BookingDto.java b/common/src/main/java/ru/practicum/booking/BookingDto.java similarity index 68% rename from src/main/java/ru/practicum/shareit/booking/BookingDto.java rename to common/src/main/java/ru/practicum/booking/BookingDto.java index c0e95e4..8f1d06d 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingDto.java +++ b/common/src/main/java/ru/practicum/booking/BookingDto.java @@ -1,11 +1,11 @@ -package ru.practicum.shareit.booking; +package ru.practicum.booking; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import ru.practicum.shareit.item.ItemDto; -import ru.practicum.shareit.user.UserDto; +import ru.practicum.item.ItemDto; +import ru.practicum.user.UserDto; import java.time.LocalDateTime; @@ -19,5 +19,5 @@ public class BookingDto { private UserDto booker; private LocalDateTime start; private LocalDateTime end; - private Booking.BookingStatus status; + private BookingStatus status; } \ No newline at end of file diff --git a/common/src/main/java/ru/practicum/booking/BookingStatus.java b/common/src/main/java/ru/practicum/booking/BookingStatus.java new file mode 100644 index 0000000..9ac8c55 --- /dev/null +++ b/common/src/main/java/ru/practicum/booking/BookingStatus.java @@ -0,0 +1,8 @@ +package ru.practicum.booking; + +public enum BookingStatus { + WAITING, + APPROVED, + REJECTED, + CANCELED +} \ No newline at end of file diff --git a/common/src/main/java/ru/practicum/booking/NewBookingRequestDto.java b/common/src/main/java/ru/practicum/booking/NewBookingRequestDto.java new file mode 100644 index 0000000..97a095e --- /dev/null +++ b/common/src/main/java/ru/practicum/booking/NewBookingRequestDto.java @@ -0,0 +1,33 @@ +package ru.practicum.booking; + +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class NewBookingRequestDto { + @NotNull + private Long itemId; + + @NotNull + @Future + private LocalDateTime start; + + @NotNull + @Future + private LocalDateTime end; + + @AssertTrue(message = "Выберите корректный срок аренды.") + public boolean isEndAfterStart() { + return end.isAfter(start); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/ItemDto.java b/common/src/main/java/ru/practicum/item/ItemDto.java similarity index 55% rename from src/main/java/ru/practicum/shareit/item/ItemDto.java rename to common/src/main/java/ru/practicum/item/ItemDto.java index 90fcb2c..9ffdf5c 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemDto.java +++ b/common/src/main/java/ru/practicum/item/ItemDto.java @@ -1,11 +1,9 @@ -package ru.practicum.shareit.item; +package ru.practicum.item; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.Data; -import ru.practicum.shareit.booking.BookingDto; -import ru.practicum.shareit.item.comment.CommentDto; +import ru.practicum.booking.BookingDto; +import ru.practicum.item.comment.CommentDto; import java.util.List; @@ -13,13 +11,8 @@ @Builder public class ItemDto { private Long id; - - @NotNull - @NotBlank private String name; - @NotNull private String description; - @NotNull private Boolean available; private Long requestId; private BookingDto lastBooking; diff --git a/common/src/main/java/ru/practicum/item/NewItemRequest.java b/common/src/main/java/ru/practicum/item/NewItemRequest.java new file mode 100644 index 0000000..b04665e --- /dev/null +++ b/common/src/main/java/ru/practicum/item/NewItemRequest.java @@ -0,0 +1,26 @@ +package ru.practicum.item; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class NewItemRequest { + + @NotBlank + private String name; + + @NotBlank + private String description; + + @NotNull + private Boolean available; + + private Long requestId; +} \ No newline at end of file diff --git a/common/src/main/java/ru/practicum/item/UpdateItemRequest.java b/common/src/main/java/ru/practicum/item/UpdateItemRequest.java new file mode 100644 index 0000000..8680083 --- /dev/null +++ b/common/src/main/java/ru/practicum/item/UpdateItemRequest.java @@ -0,0 +1,16 @@ +package ru.practicum.item; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UpdateItemRequest { + private String name; + private String description; + private Boolean available; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/comment/CommentDto.java b/common/src/main/java/ru/practicum/item/comment/CommentDto.java similarity index 84% rename from src/main/java/ru/practicum/shareit/item/comment/CommentDto.java rename to common/src/main/java/ru/practicum/item/comment/CommentDto.java index ee2fcb3..517267e 100644 --- a/src/main/java/ru/practicum/shareit/item/comment/CommentDto.java +++ b/common/src/main/java/ru/practicum/item/comment/CommentDto.java @@ -1,4 +1,4 @@ -package ru.practicum.shareit.item.comment; +package ru.practicum.item.comment; import lombok.Builder; import lombok.Data; diff --git a/common/src/main/java/ru/practicum/item/comment/NewCommentRequest.java b/common/src/main/java/ru/practicum/item/comment/NewCommentRequest.java new file mode 100644 index 0000000..ad6203c --- /dev/null +++ b/common/src/main/java/ru/practicum/item/comment/NewCommentRequest.java @@ -0,0 +1,15 @@ +package ru.practicum.item.comment; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class NewCommentRequest { + + @NotBlank + private String text; +} \ No newline at end of file diff --git a/common/src/main/java/ru/practicum/request/CreateItemRequestDto.java b/common/src/main/java/ru/practicum/request/CreateItemRequestDto.java new file mode 100644 index 0000000..5be7b66 --- /dev/null +++ b/common/src/main/java/ru/practicum/request/CreateItemRequestDto.java @@ -0,0 +1,17 @@ +package ru.practicum.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class CreateItemRequestDto { + + @NotBlank + private String description; +} \ No newline at end of file diff --git a/common/src/main/java/ru/practicum/request/ItemRequestDto.java b/common/src/main/java/ru/practicum/request/ItemRequestDto.java new file mode 100644 index 0000000..bab3fb7 --- /dev/null +++ b/common/src/main/java/ru/practicum/request/ItemRequestDto.java @@ -0,0 +1,19 @@ +package ru.practicum.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ItemRequestDto { + private Long id; + private String description; + private Long requesterId; + private LocalDateTime created; +} \ No newline at end of file diff --git a/common/src/main/java/ru/practicum/user/NewUserRequest.java b/common/src/main/java/ru/practicum/user/NewUserRequest.java new file mode 100644 index 0000000..8834aaa --- /dev/null +++ b/common/src/main/java/ru/practicum/user/NewUserRequest.java @@ -0,0 +1,19 @@ +package ru.practicum.user; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@AllArgsConstructor +public class NewUserRequest { + + @NotBlank + private String name; + + @Email(message = "Некорректный email") + private String email; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/UserDto.java b/common/src/main/java/ru/practicum/user/UpdateUserRequest.java similarity index 67% rename from src/main/java/ru/practicum/shareit/user/UserDto.java rename to common/src/main/java/ru/practicum/user/UpdateUserRequest.java index 5a67957..55a69ae 100644 --- a/src/main/java/ru/practicum/shareit/user/UserDto.java +++ b/common/src/main/java/ru/practicum/user/UpdateUserRequest.java @@ -1,15 +1,17 @@ -package ru.practicum.shareit.user; +package ru.practicum.user; import jakarta.validation.constraints.Email; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @Data @Builder -public class UserDto { +@AllArgsConstructor +public class UpdateUserRequest { private Long id; + private String name; @Email(message = "Некорректный email") private String email; - private String name; } \ No newline at end of file diff --git a/common/src/main/java/ru/practicum/user/UserDto.java b/common/src/main/java/ru/practicum/user/UserDto.java new file mode 100644 index 0000000..6fc0a2a --- /dev/null +++ b/common/src/main/java/ru/practicum/user/UserDto.java @@ -0,0 +1,16 @@ +package ru.practicum.user; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserDto { + private Long id; + private String email; + private String name; +} \ No newline at end of file diff --git a/common/src/main/resources/application.properties b/common/src/main/resources/application.properties new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b768329 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ +version: '3.8' + +services: + postgres: + image: postgres:16 + environment: + POSTGRES_USER: dbuser + POSTGRES_PASSWORD: 12345 + POSTGRES_DB: shareit + ports: + - "5432:5432" + volumes: + - pg_data:/var/lib/postgresql/data + + server: + build: + context: ./server + ports: + - "9090:9090" # <-- теперь 9090! + depends_on: + - postgres + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/shareit + SPRING_DATASOURCE_USERNAME: dbuser + SPRING_DATASOURCE_PASSWORD: 12345 + SERVER_PORT: 9090 # <-- важно! Или укажи server.port в application.properties + TZ: "Asia/Yekaterinburg" + + gateway: + build: + context: ./gateway + ports: + - "8080:8080" # <-- оставляем 8080! + depends_on: + - server + environment: + SHAREIT_SERVER_URL: http://server:9090 # <-- важно! Ссылаемся на server с новым портом + +volumes: + pg_data: diff --git a/gateway/Dockerfile b/gateway/Dockerfile new file mode 100644 index 0000000..6a005ee --- /dev/null +++ b/gateway/Dockerfile @@ -0,0 +1,7 @@ +FROM openjdk:21-jdk-slim + +WORKDIR /app + +COPY target/gateway-0.0.1-SNAPSHOT.jar app.jar + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/gateway/pom.xml b/gateway/pom.xml new file mode 100644 index 0000000..265bcf9 --- /dev/null +++ b/gateway/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + + ru.practicum + shareit + 0.0.1-SNAPSHOT + + + gateway + + + 21 + 21 + UTF-8 + + + + + ru.practicum + common + 0.0.1-SNAPSHOT + + + + 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 + + ru.practicum.ShareItGateway + + + + + repackage + + + + + + + \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/ShareItGateway.java b/gateway/src/main/java/ru/practicum/ShareItGateway.java new file mode 100644 index 0000000..6fe87bd --- /dev/null +++ b/gateway/src/main/java/ru/practicum/ShareItGateway.java @@ -0,0 +1,20 @@ +package ru.practicum; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; + +//@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) +@SpringBootApplication( + exclude = { + DataSourceAutoConfiguration.class, + HibernateJpaAutoConfiguration.class + } +) +public class ShareItGateway { + public static void main(String[] args) { + SpringApplication.run(ShareItGateway.class, args); + } + +} diff --git a/gateway/src/main/java/ru/practicum/booking/BookingClient.java b/gateway/src/main/java/ru/practicum/booking/BookingClient.java new file mode 100644 index 0000000..3c88290 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/booking/BookingClient.java @@ -0,0 +1,61 @@ +package ru.practicum.booking; + +import lombok.extern.slf4j.Slf4j; +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.client.BaseClient; + +import java.util.Map; + +@Slf4j +@Service +public class BookingClient extends BaseClient { + + private static final String API_PREFIX = "/bookings"; + + + public BookingClient(@Value("${shareit.server.url}") String serverUrl, RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + API_PREFIX)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + + } + + public ResponseEntity create(Long userId, NewBookingRequestDto bookingDto) { + return post("", userId, bookingDto); + } + + public ResponseEntity approve(Long userId, Long bookingId, Boolean approved) { + Map params = Map.of("approved", approved); + return patch("/" + bookingId + "?approved={approved}", userId, params, null); + } + + public ResponseEntity getById(Long userId, Long bookingId) { + return get("/" + bookingId, userId); + } + + public ResponseEntity getBookings(Long userId, String state, Integer from, Integer size) { + Map params = Map.of( + "state", state, + "from", from, + "size", size + ); + return get("?state={state}&from={from}&size={size}", userId, params); + } + + public ResponseEntity getOwnerBookings(Long userId, String state, Integer from, Integer size) { + Map params = Map.of( + "state", state, + "from", from, + "size", size + ); + return get("/owner?state={state}&from={from}&size={size}", userId, params); + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/booking/BookingController.java b/gateway/src/main/java/ru/practicum/booking/BookingController.java new file mode 100644 index 0000000..ad4e00b --- /dev/null +++ b/gateway/src/main/java/ru/practicum/booking/BookingController.java @@ -0,0 +1,52 @@ +package ru.practicum.booking; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/bookings") +@RequiredArgsConstructor +public class BookingController { + + private final BookingClient bookingClient; + private static final String USER_HEADER = "X-Sharer-User-Id"; + + @PostMapping + public ResponseEntity create(@RequestHeader(USER_HEADER) Long userId, + @Valid @RequestBody NewBookingRequestDto bookingDto) { + return bookingClient.create(userId, bookingDto); + } + + @PatchMapping("/{bookingId}") + public ResponseEntity approve(@RequestHeader(USER_HEADER) Long userId, + @PathVariable Long bookingId, + @RequestParam Boolean approved) { + return bookingClient.approve(userId, bookingId, approved); + } + + @GetMapping("/{bookingId}") + public ResponseEntity getById(@RequestHeader(USER_HEADER) Long userId, + @PathVariable Long bookingId) { + return bookingClient.getById(userId, bookingId); + } + + @GetMapping + public ResponseEntity getBookings(@RequestHeader(USER_HEADER) Long userId, + @RequestParam(defaultValue = "ALL") String state, + @RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + return bookingClient.getBookings(userId, state, from, size); + } + + @GetMapping("/owner") + public ResponseEntity getOwnerBookings(@RequestHeader(USER_HEADER) Long userId, + @RequestParam(defaultValue = "ALL") String state, + @RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + return bookingClient.getOwnerBookings(userId, state, from, size); + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/client/BaseClient.java b/gateway/src/main/java/ru/practicum/client/BaseClient.java new file mode 100644 index 0000000..ba049de --- /dev/null +++ b/gateway/src/main/java/ru/practicum/client/BaseClient.java @@ -0,0 +1,121 @@ +package ru.practicum.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/item/ItemClient.java b/gateway/src/main/java/ru/practicum/item/ItemClient.java new file mode 100644 index 0000000..7e60ea0 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/item/ItemClient.java @@ -0,0 +1,49 @@ +package ru.practicum.item; + + +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.client.BaseClient; +import ru.practicum.item.comment.NewCommentRequest; + +@Service +public class ItemClient extends BaseClient { + private static final String API_PREFIX = "/items"; + + public ItemClient(@Value("${shareit.server.url}") String serverUrl, RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + API_PREFIX)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + } + + public ResponseEntity create(Long userId, NewItemRequest newItemRequest) { + return post("", userId, newItemRequest); + } + + public ResponseEntity update(Long userId, Long itemId, UpdateItemRequest updateItemRequest) { + return patch("/" + itemId, userId, updateItemRequest); + } + + public ResponseEntity getById(Long itemId, Long userId) { + return get("/" + itemId, userId); + } + + public ResponseEntity getAllByOwner(Long userId) { + return get("", userId); + } + + public ResponseEntity search(String text) { + return get("/search?text=" + text); + } + + public ResponseEntity addComment(Long userId, Long itemId, NewCommentRequest newCommentRequest) { + return post("/" + itemId + "/comment", userId, newCommentRequest); + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/item/ItemController.java b/gateway/src/main/java/ru/practicum/item/ItemController.java new file mode 100644 index 0000000..39d3d99 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/item/ItemController.java @@ -0,0 +1,52 @@ +package ru.practicum.item; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.practicum.item.comment.NewCommentRequest; + +@RestController +@RequestMapping("/items") +@RequiredArgsConstructor +public class ItemController { + private final ItemClient itemClient; + + private static final String USER_HEADER = "X-Sharer-User-Id"; + + @PostMapping + public ResponseEntity create(@RequestHeader(USER_HEADER) Long userId, + @Valid @RequestBody NewItemRequest newItemRequest) { + return itemClient.create(userId, newItemRequest); + } + + @PatchMapping("/{itemId}") + public ResponseEntity update(@RequestHeader(USER_HEADER) Long userId, + @PathVariable Long itemId, + @RequestBody UpdateItemRequest updateItemRequest) { + return itemClient.update(userId, itemId, updateItemRequest); + } + + @GetMapping("/{itemId}") + public ResponseEntity getById(@PathVariable Long itemId, + @RequestHeader(value = "X-Sharer-User-Id", required = false) Long userId) { + return itemClient.getById(itemId, userId); + } + + @GetMapping + public ResponseEntity getAllByOwner(@RequestHeader(USER_HEADER) Long userId) { + return itemClient.getAllByOwner(userId); + } + + @GetMapping("/search") + public ResponseEntity search(@RequestParam String text) { + return itemClient.search(text); + } + + @PostMapping("/{itemId}/comment") + public ResponseEntity addComment(@RequestHeader("X-Sharer-User-Id") Long userId, + @PathVariable Long itemId, + @Valid @RequestBody NewCommentRequest newCommentRequest) { + return itemClient.addComment(userId, itemId, newCommentRequest); + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/request/ItemRequestClient.java b/gateway/src/main/java/ru/practicum/request/ItemRequestClient.java new file mode 100644 index 0000000..c10c0e4 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/request/ItemRequestClient.java @@ -0,0 +1,40 @@ +package ru.practicum.request; + +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.client.BaseClient; + +@Service +public class ItemRequestClient extends BaseClient { + private static final String API_PREFIX = "/requests"; + + public ItemRequestClient(@Value("${shareit.server.url}") String serverUrl, + RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + API_PREFIX)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + } + + public ResponseEntity create(Long userId, CreateItemRequestDto createItemRequestDto) { + return post("", userId, createItemRequestDto); + } + + public ResponseEntity getOwn(Long userId) { + return get("", userId); + } + + public ResponseEntity getAll(Long userId, int from, int size) { + return get("/all?from=" + from + "&size=" + size, userId); + } + + public ResponseEntity getById(Long userId, Long requestId) { + return get("/" + requestId, userId); + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/request/ItemRequestController.java b/gateway/src/main/java/ru/practicum/request/ItemRequestController.java new file mode 100644 index 0000000..6dc8609 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/request/ItemRequestController.java @@ -0,0 +1,41 @@ +package ru.practicum.request; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequestMapping(path = "/requests") +@RequiredArgsConstructor +public class ItemRequestController { + + private final ItemRequestClient itemRequestClient; + + private static final String USER_HEADER = "X-Sharer-User-Id"; + + @PostMapping + public ResponseEntity create(@RequestHeader(USER_HEADER) Long userId, + @Valid @RequestBody CreateItemRequestDto createItemRequestDto) { + return itemRequestClient.create(userId, createItemRequestDto); + } + + @GetMapping + public ResponseEntity getOwn(@RequestHeader(USER_HEADER) Long userId) { + return itemRequestClient.getOwn(userId); + } + + @GetMapping("/all") + public ResponseEntity getAll(@RequestHeader(USER_HEADER) Long userId, + @RequestParam int from, + @RequestParam int size) { + return itemRequestClient.getAll(userId, from, size); + } + + @GetMapping("/{requestId}") + public ResponseEntity getById(@RequestHeader(USER_HEADER) Long userId, + @PathVariable Long requestId) { + return itemRequestClient.getById(userId, requestId); + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/user/UserClient.java b/gateway/src/main/java/ru/practicum/user/UserClient.java new file mode 100644 index 0000000..5fd1fa2 --- /dev/null +++ b/gateway/src/main/java/ru/practicum/user/UserClient.java @@ -0,0 +1,43 @@ +package ru.practicum.user; + +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.client.BaseClient; + +@Service +public class UserClient extends BaseClient { + private static final String API_PREFIX = "/users"; + + public UserClient(@Value("${shareit.server.url}") String serverUrl, RestTemplateBuilder builder) { + super( + builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl + API_PREFIX)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build() + ); + } + + public ResponseEntity create(NewUserRequest newUserRequest) { + return post("", newUserRequest); + } + + public ResponseEntity update(Long userId, UpdateUserRequest updateUserRequest) { + return patch("/" + userId, updateUserRequest); + } + + public ResponseEntity getById(Long userId) { + return get("/" + userId); + } + + public ResponseEntity getAll() { + return get(""); + } + + public ResponseEntity delete(Long userId) { + return super.delete("/" + userId); + } +} \ No newline at end of file diff --git a/gateway/src/main/java/ru/practicum/user/UserController.java b/gateway/src/main/java/ru/practicum/user/UserController.java new file mode 100644 index 0000000..98993dc --- /dev/null +++ b/gateway/src/main/java/ru/practicum/user/UserController.java @@ -0,0 +1,40 @@ +package ru.practicum.user; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/users") +@RequiredArgsConstructor +public class UserController { + + private final UserClient userClient; + + @PostMapping + public ResponseEntity create(@Valid @RequestBody NewUserRequest newUserRequest) { + return userClient.create(newUserRequest); + } + + @PatchMapping("/{userId}") + public ResponseEntity update(@PathVariable Long userId, + @Valid @RequestBody UpdateUserRequest updateUserRequest) { + return userClient.update(userId, updateUserRequest); + } + + @GetMapping("/{userId}") + public ResponseEntity getById(@PathVariable Long userId) { + return userClient.getById(userId); + } + + @GetMapping + public ResponseEntity getAll() { + return userClient.getAll(); + } + + @DeleteMapping("/{userId}") + public ResponseEntity delete(@PathVariable Long userId) { + return userClient.delete(userId); + } +} \ No newline at end of file diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties new file mode 100644 index 0000000..0b4bc7d --- /dev/null +++ b/gateway/src/main/resources/application.properties @@ -0,0 +1,5 @@ +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/pom.xml b/pom.xml index 64504e8..cab9232 100644 --- a/pom.xml +++ b/pom.xml @@ -12,82 +12,37 @@ ru.practicum shareit 0.0.1-SNAPSHOT + pom + + + common + server + gateway + - ShareIt 21 - - org.springframework.boot - spring-boot-starter-data-jpa - - - - org.postgresql - postgresql - 42.7.5 - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-configuration-processor - true - - - org.projectlombok - lombok - true - - - - com.h2database - h2 - test - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-starter-validation - - - - - - src/main/resources - true - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - + + org.springframework.boot + spring-boot-maven-plugin + + + true + + + + org.projectlombok + lombok + + + + org.apache.maven.plugins maven-surefire-plugin @@ -234,17 +189,5 @@ - - coverage - - - - org.jacoco - jacoco-maven-plugin - - - - - - + \ No newline at end of file diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..62fa95c --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,11 @@ +# Используем официальный образ Java +FROM openjdk:21-jdk-slim + +# Рабочая папка внутри контейнера +WORKDIR /app + +# Копируем собранный jar +COPY target/server-0.0.1-SNAPSHOT.jar app.jar + +# Команда запуска +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/server/pom.xml b/server/pom.xml new file mode 100644 index 0000000..0b37d19 --- /dev/null +++ b/server/pom.xml @@ -0,0 +1,106 @@ + + + 4.0.0 + + ru.practicum + shareit + 0.0.1-SNAPSHOT + + + server + + + 21 + 21 + UTF-8 + + + + + ru.practicum + common + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.postgresql + postgresql + runtime + + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + ru.practicum.shareit.ShareItServer + + + + + + + + + coverage + + + + org.jacoco + jacoco-maven-plugin + + + + + + \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/ShareItApp.java b/server/src/main/java/ru/practicum/shareit/ShareItServer.java similarity index 59% rename from src/main/java/ru/practicum/shareit/ShareItApp.java rename to server/src/main/java/ru/practicum/shareit/ShareItServer.java index 1137780..1b8588f 100644 --- a/src/main/java/ru/practicum/shareit/ShareItApp.java +++ b/server/src/main/java/ru/practicum/shareit/ShareItServer.java @@ -4,9 +4,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class ShareItApp { +public class ShareItServer { public static void main(String[] args) { - SpringApplication.run(ShareItApp.class, args); + SpringApplication.run(ShareItServer.class, args); + System.out.println("PORT: " + System.getProperty("server.port")); } } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/Booking.java b/server/src/main/java/ru/practicum/shareit/booking/Booking.java similarity index 91% rename from src/main/java/ru/practicum/shareit/booking/Booking.java rename to server/src/main/java/ru/practicum/shareit/booking/Booking.java index 8326963..946d6cb 100644 --- a/src/main/java/ru/practicum/shareit/booking/Booking.java +++ b/server/src/main/java/ru/practicum/shareit/booking/Booking.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.*; +import ru.practicum.booking.BookingStatus; import ru.practicum.shareit.item.Item; import ru.practicum.shareit.user.User; @@ -16,13 +17,6 @@ @Builder public class Booking { - public enum BookingStatus { - WAITING, - APPROVED, - REJECTED, - CANCELED - } - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/src/main/java/ru/practicum/shareit/booking/BookingController.java b/server/src/main/java/ru/practicum/shareit/booking/BookingController.java similarity index 73% rename from src/main/java/ru/practicum/shareit/booking/BookingController.java rename to server/src/main/java/ru/practicum/shareit/booking/BookingController.java index 4264461..bb709e0 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingController.java +++ b/server/src/main/java/ru/practicum/shareit/booking/BookingController.java @@ -1,8 +1,11 @@ package ru.practicum.shareit.booking; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; +import ru.practicum.booking.BookingDto; +import ru.practicum.booking.NewBookingRequestDto; import java.util.List; @@ -15,36 +18,32 @@ public class BookingController { @PostMapping public BookingDto createBooking(@RequestHeader("X-Sharer-User-Id") Long userId, - @RequestBody BookingRequestDto request) { - return BookingMapper.toDto(bookingService.createBooking(userId, request)); + @Valid @RequestBody NewBookingRequestDto request) { + return bookingService.createBooking(userId, request); } @PatchMapping("/{bookingId}") public BookingDto approveBooking(@RequestHeader("X-Sharer-User-Id") Long ownerId, @PathVariable Long bookingId, @RequestParam boolean approved) { - return BookingMapper.toDto(bookingService.approveBooking(ownerId, bookingId, approved)); + return bookingService.approveBooking(ownerId, bookingId, approved); } @GetMapping("/{bookingId}") public BookingDto getBookingById(@RequestHeader("X-Sharer-User-Id") Long userId, @PathVariable Long bookingId) { - return BookingMapper.toDto(bookingService.getBookingById(userId, bookingId)); + return bookingService.getBookingById(userId, bookingId); } @GetMapping public List getBookingsByUser(@RequestHeader("X-Sharer-User-Id") Long userId, @RequestParam(defaultValue = "ALL") String state) { - return bookingService.getBookingsByUser(userId, BookingState.from(state)).stream() - .map(BookingMapper::toDto) - .toList(); + return bookingService.getBookingsByUser(userId, BookingState.from(state)); } @GetMapping("/owner") public List getBookingsByOwner(@RequestHeader("X-Sharer-User-Id") Long userId, @RequestParam(defaultValue = "ALL") String state) { - return bookingService.getBookingsByOwner(userId, BookingState.from(state)).stream() - .map(BookingMapper::toDto) - .toList(); + return bookingService.getBookingsByOwner(userId, BookingState.from(state)); } } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/BookingMapper.java b/server/src/main/java/ru/practicum/shareit/booking/BookingMapper.java similarity index 76% rename from src/main/java/ru/practicum/shareit/booking/BookingMapper.java rename to server/src/main/java/ru/practicum/shareit/booking/BookingMapper.java index 2e7e531..12fae06 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingMapper.java +++ b/server/src/main/java/ru/practicum/shareit/booking/BookingMapper.java @@ -1,18 +1,21 @@ package ru.practicum.shareit.booking; +import ru.practicum.booking.BookingDto; +import ru.practicum.booking.NewBookingRequestDto; +import ru.practicum.booking.BookingStatus; import ru.practicum.shareit.item.Item; import ru.practicum.shareit.item.ItemMapper; import ru.practicum.shareit.user.User; import ru.practicum.shareit.user.UserMapper; public class BookingMapper { - public static Booking toBooking(BookingRequestDto dto, Item item, User booker) { + public static Booking toBooking(NewBookingRequestDto dto, Item item, User booker) { return Booking.builder() .start(dto.getStart()) .end(dto.getEnd()) .item(item) .booker(booker) - .status(Booking.BookingStatus.WAITING) + .status(BookingStatus.WAITING) .build(); } diff --git a/src/main/java/ru/practicum/shareit/booking/BookingRepository.java b/server/src/main/java/ru/practicum/shareit/booking/BookingRepository.java similarity index 96% rename from src/main/java/ru/practicum/shareit/booking/BookingRepository.java rename to server/src/main/java/ru/practicum/shareit/booking/BookingRepository.java index ca6f4d3..c2492d0 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingRepository.java +++ b/server/src/main/java/ru/practicum/shareit/booking/BookingRepository.java @@ -3,6 +3,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import ru.practicum.booking.BookingStatus; import java.time.LocalDateTime; import java.util.List; @@ -40,7 +41,7 @@ List findOverlappingBookings(@Param("itemId") Long itemId, List findByItemOwnerIdOrderByStartDesc(Long ownerId); - List findByBookerIdAndStatusOrderByStartDesc(Long bookerId, Booking.BookingStatus status); + List findByBookerIdAndStatusOrderByStartDesc(Long bookerId, BookingStatus status); List findByBookerIdAndStartAfterOrderByStartDesc(Long bookerId, LocalDateTime now); @@ -53,5 +54,5 @@ List findByItemOwnerIdAndStartBeforeAndEndAfterOrderByStartDesc( Long ownerId, LocalDateTime now1, LocalDateTime now2); boolean existsByItemIdAndBookerIdAndEndBeforeAndStatus( - Long itemId, Long userId, LocalDateTime now, Booking.BookingStatus status); + Long itemId, Long userId, LocalDateTime now, BookingStatus status); } \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/booking/BookingService.java b/server/src/main/java/ru/practicum/shareit/booking/BookingService.java new file mode 100644 index 0000000..a07f0a3 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/booking/BookingService.java @@ -0,0 +1,20 @@ +package ru.practicum.shareit.booking; + + +import ru.practicum.booking.BookingDto; +import ru.practicum.booking.NewBookingRequestDto; + +import java.util.List; + +public interface BookingService { + + BookingDto createBooking(Long userId, NewBookingRequestDto dto); + + BookingDto approveBooking(Long ownerId, Long bookingId, boolean approved); + + BookingDto getBookingById(Long userId, Long bookingId); + + List getBookingsByUser(Long userId, BookingState state); + + List getBookingsByOwner(Long ownerId, BookingState state); +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java b/server/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java similarity index 70% rename from src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java rename to server/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java index 52fc38b..dadc94f 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java +++ b/server/src/main/java/ru/practicum/shareit/booking/BookingServiceImpl.java @@ -4,12 +4,15 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import ru.practicum.booking.BookingDto; +import ru.practicum.booking.NewBookingRequestDto; +import ru.practicum.booking.BookingStatus; import ru.practicum.shareit.exception.ConflictException; import ru.practicum.shareit.exception.ForbiddenException; import ru.practicum.shareit.exception.NotFoundException; import ru.practicum.shareit.exception.ValidationException; import ru.practicum.shareit.item.Item; -import ru.practicum.shareit.item.ItemService; +import ru.practicum.shareit.item.ItemRepository; import ru.practicum.shareit.user.User; import ru.practicum.shareit.user.UserService; @@ -23,13 +26,15 @@ @Service public class BookingServiceImpl implements BookingService { private final BookingRepository bookingRepository; + private final ItemRepository itemRepository; private final UserService userService; - private final ItemService itemService; @Override - public Booking createBooking(Long userId, BookingRequestDto dto) { - User booker = userService.getUserById(userId); - Item item = itemService.getItemById(dto.getItemId(), userId); + public BookingDto createBooking(Long userId, NewBookingRequestDto dto) { + User booker = userService.getEntityById(userId); + Item item = itemRepository.findById(dto.getItemId()) + .orElseThrow(() -> new NotFoundException("Вещь с id " + dto.getItemId() + " не найдена.")); + if (!item.getAvailable()) { throw new ValidationException("Вещь недоступна для бронирования."); @@ -41,30 +46,30 @@ public Booking createBooking(Long userId, BookingRequestDto dto) { } Booking booking = BookingMapper.toBooking(dto, item, booker); - booking.setStatus(Booking.BookingStatus.WAITING); + booking.setStatus(BookingStatus.WAITING); log.info("Создано бронирование на вещь с ID {}", item.getId()); - return bookingRepository.save(booking); + return BookingMapper.toDto(bookingRepository.save(booking)); } @Override - public Booking approveBooking(Long ownerId, Long bookingId, boolean approved) { + public BookingDto approveBooking(Long ownerId, Long bookingId, boolean approved) { Booking booking = getBookingOrThrow(bookingId); if (!Objects.equals(booking.getItem().getOwner().getId(), ownerId)) { throw new ForbiddenException("Только владелец может подтверждать бронирование"); } - if (booking.getStatus() != Booking.BookingStatus.WAITING) { + if (booking.getStatus() != BookingStatus.WAITING) { throw new ConflictException("Бронирование уже подтверждено или отклонено"); } - booking.setStatus(approved ? Booking.BookingStatus.APPROVED : Booking.BookingStatus.REJECTED); - return bookingRepository.save(booking); + booking.setStatus(approved ? BookingStatus.APPROVED : BookingStatus.REJECTED); + return BookingMapper.toDto(bookingRepository.save(booking)); } @Override @Transactional(readOnly = true) - public Booking getBookingById(Long userId, Long bookingId) { + public BookingDto getBookingById(Long userId, Long bookingId) { Booking booking = getBookingOrThrow(bookingId); Long bookerId = booking.getBooker().getId(); Long ownerId = booking.getItem().getOwner().getId(); @@ -73,21 +78,27 @@ public Booking getBookingById(Long userId, Long bookingId) { throw new NotFoundException("Нет доступа к бронированию"); } - return booking; + return BookingMapper.toDto(booking); } @Override @Transactional(readOnly = true) - public List getBookingsByUser(Long userId, BookingState state) { + public List getBookingsByUser(Long userId, BookingState state) { userService.getUserById(userId); - return findByState(userId, state, true); + + return findByState(userId, state, true).stream() + .map(BookingMapper::toDto) + .toList(); } @Override @Transactional(readOnly = true) - public List getBookingsByOwner(Long ownerId, BookingState state) { + public List getBookingsByOwner(Long ownerId, BookingState state) { userService.getUserById(ownerId); - return findByState(ownerId, state, false); + + return findByState(ownerId, state, false).stream() + .map(BookingMapper::toDto) + .toList(); } private List findByState(Long id, BookingState state, boolean byBooker) { @@ -102,9 +113,9 @@ private List findByState(Long id, BookingState state, boolean byBooker) case FUTURE -> bookingRepository.findByBookerIdAndStartAfterOrderByStartDesc(id, now); case PAST -> bookingRepository.findByBookerIdAndEndBeforeOrderByStartDesc(id, now); case WAITING -> bookingRepository.findByBookerIdAndStatusOrderByStartDesc(id, - Booking.BookingStatus.WAITING); + BookingStatus.WAITING); case REJECTED -> bookingRepository.findByBookerIdAndStatusOrderByStartDesc(id, - Booking.BookingStatus.REJECTED); + BookingStatus.REJECTED); }; } diff --git a/src/main/java/ru/practicum/shareit/booking/BookingState.java b/server/src/main/java/ru/practicum/shareit/booking/BookingState.java similarity index 99% rename from src/main/java/ru/practicum/shareit/booking/BookingState.java rename to server/src/main/java/ru/practicum/shareit/booking/BookingState.java index 705f825..7fc2541 100644 --- a/src/main/java/ru/practicum/shareit/booking/BookingState.java +++ b/server/src/main/java/ru/practicum/shareit/booking/BookingState.java @@ -1,6 +1,5 @@ package ru.practicum.shareit.booking; - public enum BookingState { ALL, CURRENT, diff --git a/src/main/java/ru/practicum/shareit/exception/ConflictException.java b/server/src/main/java/ru/practicum/shareit/exception/ConflictException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/exception/ConflictException.java rename to server/src/main/java/ru/practicum/shareit/exception/ConflictException.java diff --git a/src/main/java/ru/practicum/shareit/exception/ForbiddenException.java b/server/src/main/java/ru/practicum/shareit/exception/ForbiddenException.java similarity index 100% rename from src/main/java/ru/practicum/shareit/exception/ForbiddenException.java rename to server/src/main/java/ru/practicum/shareit/exception/ForbiddenException.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/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java b/server/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java similarity index 98% rename from src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java rename to server/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java index 4dc6c4a..35fafe0 100644 --- a/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java +++ b/server/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java @@ -56,7 +56,7 @@ public ResponseEntity handleConflict( } @ExceptionHandler({ForbiddenException.class}) - public ResponseEntity handleConflict( + public ResponseEntity handleForbidden( ForbiddenException e, HttpServletRequest request) { ErrorResponse errorResponse = new ErrorResponse( LocalDateTime.now(), diff --git a/src/main/java/ru/practicum/shareit/item/Item.java b/server/src/main/java/ru/practicum/shareit/item/Item.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/Item.java rename to server/src/main/java/ru/practicum/shareit/item/Item.java diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/server/src/main/java/ru/practicum/shareit/item/ItemController.java similarity index 55% rename from src/main/java/ru/practicum/shareit/item/ItemController.java rename to server/src/main/java/ru/practicum/shareit/item/ItemController.java index d3dd252..c26cbec 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/server/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -3,8 +3,11 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; -import ru.practicum.shareit.item.comment.CommentDto; -import ru.practicum.shareit.item.comment.CommentMapper; +import ru.practicum.item.ItemDto; +import ru.practicum.item.comment.CommentDto; +import ru.practicum.item.NewItemRequest; +import ru.practicum.item.UpdateItemRequest; +import ru.practicum.item.comment.NewCommentRequest; import java.util.List; @@ -19,46 +22,37 @@ public class ItemController { @PostMapping public ItemDto create(@RequestHeader(USER_HEADER) Long userId, - @Valid @RequestBody ItemDto itemDto) { - return ItemMapper.toItemDto(itemService.create(userId, itemDto)); + @Valid @RequestBody NewItemRequest newItemRequest) { + return itemService.create(userId, newItemRequest); } @PatchMapping("/{itemId}") public ItemDto update(@RequestHeader(USER_HEADER) Long userId, @PathVariable Long itemId, - @RequestBody ItemDto dto) { - return ItemMapper.toItemDto(itemService.update(userId, itemId, dto)); + @RequestBody UpdateItemRequest updateItemRequest) { + return itemService.update(userId, itemId, updateItemRequest); } @GetMapping("/{itemId}") public ItemDto getById(@PathVariable Long itemId, @RequestHeader(value = "X-Sharer-User-Id", required = false) Long userId) { - Item item = itemService.getItemById(itemId, userId); - if (userId == null) { - return ItemMapper.toItemDto(item); - } - - return ItemMapper.toItemDto(item, userId); + return itemService.getItemById(itemId, userId); } @GetMapping public List getAllByOwner(@RequestHeader(USER_HEADER) Long userId) { - return itemService.getAllByOwner(userId).stream() - .map(ItemMapper::toItemDto) - .toList(); + return itemService.getAllByOwner(userId); } @GetMapping("/search") public List search(@RequestParam String text) { - return itemService.search(text).stream() - .map(ItemMapper::toItemDto) - .toList(); + return itemService.search(text); } @PostMapping("/{itemId}/comment") public CommentDto addComment(@RequestHeader("X-Sharer-User-Id") Long userId, @PathVariable Long itemId, - @RequestBody CommentDto newComment) { - return CommentMapper.toDto(itemService.addComment(userId, itemId, newComment)); + @Valid @RequestBody NewCommentRequest newCommentRequest) { + return itemService.addComment(userId, itemId, newCommentRequest); } } \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/ItemMapper.java b/server/src/main/java/ru/practicum/shareit/item/ItemMapper.java similarity index 61% rename from src/main/java/ru/practicum/shareit/item/ItemMapper.java rename to server/src/main/java/ru/practicum/shareit/item/ItemMapper.java index 83a07de..f45c682 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemMapper.java +++ b/server/src/main/java/ru/practicum/shareit/item/ItemMapper.java @@ -1,5 +1,8 @@ package ru.practicum.shareit.item; +import ru.practicum.item.ItemDto; +import ru.practicum.item.NewItemRequest; +import ru.practicum.item.UpdateItemRequest; import ru.practicum.shareit.booking.BookingMapper; import ru.practicum.shareit.item.comment.CommentMapper; import ru.practicum.shareit.user.User; @@ -19,6 +22,14 @@ public static ItemDto toItemDto(Item item) { .build(); } + public static ItemShortDto toShortItemDto(Item item) { + return ItemShortDto.builder() + .id(item.getId()) + .name(item.getName()) + .requestId(item.getRequestId()) + .build(); + } + public static ItemDto toItemDto(Item item, Long userId) { ItemDto.ItemDtoBuilder builder = ItemDto.builder() .id(item.getId()) @@ -43,25 +54,25 @@ public static ItemDto toItemDto(Item item, Long userId) { return builder.build(); } - public static Item toItem(ItemDto itemDto, User owner, Long requestId) { + public static Item toItem(NewItemRequest newItemRequest, User owner, Long requestId) { return Item.builder() - .name(itemDto.getName()) - .description(itemDto.getDescription()) - .available(itemDto.getAvailable()) + .name(newItemRequest.getName()) + .description(newItemRequest.getDescription()) + .available(newItemRequest.getAvailable()) .owner(owner) .requestId(requestId) .build(); } - public static Item updateItemFields(Item item, ItemDto itemDto) { - if (itemDto.getName() != null) { - item.setName(itemDto.getName()); + public static Item updateItemFields(Item item, UpdateItemRequest updateItemRequest) { + if (updateItemRequest.getName() != null) { + item.setName(updateItemRequest.getName()); } - if (itemDto.getDescription() != null) { - item.setDescription(itemDto.getDescription()); + if (updateItemRequest.getDescription() != null) { + item.setDescription(updateItemRequest.getDescription()); } - if (itemDto.getAvailable() != null) { - item.setAvailable(itemDto.getAvailable()); + if (updateItemRequest.getAvailable() != null) { + item.setAvailable(updateItemRequest.getAvailable()); } return item; } diff --git a/src/main/java/ru/practicum/shareit/item/ItemRepository.java b/server/src/main/java/ru/practicum/shareit/item/ItemRepository.java similarity index 84% rename from src/main/java/ru/practicum/shareit/item/ItemRepository.java rename to server/src/main/java/ru/practicum/shareit/item/ItemRepository.java index bbaa3ed..f3fb086 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemRepository.java +++ b/server/src/main/java/ru/practicum/shareit/item/ItemRepository.java @@ -16,4 +16,8 @@ public interface ItemRepository extends JpaRepository { AND (LOWER(i.name) LIKE %:text% OR LOWER(i.description) LIKE %:text%) """) List search(@Param("text") String text); + + List findAllByRequestIdIn(List requestIds); + + List findAllByRequestId(Long requestId); } \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/item/ItemService.java b/server/src/main/java/ru/practicum/shareit/item/ItemService.java new file mode 100644 index 0000000..8667581 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/ItemService.java @@ -0,0 +1,26 @@ +package ru.practicum.shareit.item; + +import ru.practicum.item.ItemDto; +import ru.practicum.item.comment.CommentDto; +import ru.practicum.item.NewItemRequest; +import ru.practicum.item.UpdateItemRequest; +import ru.practicum.item.comment.NewCommentRequest; + +import java.util.List; + +public interface ItemService { + + ItemDto create(Long userId, NewItemRequest newItemRequest); + + ItemDto update(Long userId, Long itemId, UpdateItemRequest updateItemRequest); + + ItemDto getItemById(Long itemId, Long requesterId); + + Item getEntityById(Long itemId); + + List getAllByOwner(Long userId); + + List search(String text); + + CommentDto addComment(Long userId, Long itemId, NewCommentRequest newCommentRequest); +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/server/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java similarity index 55% rename from src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java rename to server/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java index ef178b0..9c8ca24 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java +++ b/server/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -4,13 +4,19 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import ru.practicum.shareit.booking.Booking; +import ru.practicum.booking.BookingStatus; +import ru.practicum.item.ItemDto; +import ru.practicum.item.comment.CommentDto; +import ru.practicum.item.NewItemRequest; +import ru.practicum.item.UpdateItemRequest; +import ru.practicum.item.comment.NewCommentRequest; import ru.practicum.shareit.booking.BookingRepository; import ru.practicum.shareit.exception.NotFoundException; import ru.practicum.shareit.exception.ValidationException; import ru.practicum.shareit.item.comment.Comment; -import ru.practicum.shareit.item.comment.CommentDto; +import ru.practicum.shareit.item.comment.CommentMapper; import ru.practicum.shareit.item.comment.CommentRepository; +import ru.practicum.shareit.request.ItemRequestRepository; import ru.practicum.shareit.user.User; import ru.practicum.shareit.user.UserService; @@ -24,38 +30,47 @@ public class ItemServiceImpl implements ItemService { private final UserService userService; private final ItemRepository itemRepository; + private final ItemRequestRepository itemRequestRepository; private final BookingRepository bookingRepository; private final CommentRepository commentRepository; @Override - public Item create(Long userId, ItemDto itemDto) { - User owner = userService.getUserById(userId); - Item item = ItemMapper.toItem(itemDto, owner, null); // пока request == null + public ItemDto create(Long userId, NewItemRequest newItemRequest) { + User owner = userService.getEntityById(userId); + Long requestId = newItemRequest.getRequestId(); + + if (requestId != null) { + if (!itemRequestRepository.existsById(requestId)) { + throw new NotFoundException("Ведь с идентификатором запроса " + requestId + " не найдена."); + } + } + + Item item = ItemMapper.toItem(newItemRequest, owner, requestId); Item savedItem = itemRepository.save(item); - log.info("Вещь создана: {}", item); - return savedItem; + log.info("Вещь создана: {}", savedItem); + return ItemMapper.toItemDto(savedItem); } @Override - public Item update(Long userId, Long itemId, ItemDto itemDto) { - Item item = itemRepository.findById(itemId) - .orElseThrow(() -> new NotFoundException("Вещь с id " + itemId + " не найдена.")); + public ItemDto update(Long userId, Long itemId, UpdateItemRequest updateItemRequest) { + Item item = getEntityById(itemId); if (!Objects.equals(item.getOwner().getId(), userId)) { throw new NotFoundException("Редактировать может только владелец."); } - Item updatedItem = ItemMapper.updateItemFields(item, itemDto); + ItemMapper.updateItemFields(item, updateItemRequest); log.info("Вещь обновлена: {}", item); - return itemRepository.save(updatedItem); + itemRepository.save(item); + + return ItemMapper.toItemDto(item); } @Override @Transactional(readOnly = true) - public Item getItemById(Long itemId, Long requesterId) { - Item item = itemRepository.findById(itemId) - .orElseThrow(() -> new NotFoundException("Вещь с id " + itemId + " не найдена.")); + public ItemDto getItemById(Long itemId, Long requesterId) { + Item item = getEntityById(itemId); if (Objects.equals(item.getOwner().getId(), requesterId)) { item.setLastBooking(bookingRepository.findLastBooking(itemId, LocalDateTime.now())); @@ -65,48 +80,62 @@ public Item getItemById(Long itemId, Long requesterId) { List comments = commentRepository.findByItemIdOrderByCreatedDesc(itemId); item.setComments(comments); - return item; + return ItemMapper.toItemDto(item, requesterId); } @Override @Transactional(readOnly = true) - public List getAllByOwner(Long userId) { + public Item getEntityById(Long itemId) { + return itemRepository.findById(itemId) + .orElseThrow(() -> new NotFoundException("Вещь с id " + itemId + " не найдена.")); + } + + @Override + @Transactional(readOnly = true) + public List getAllByOwner(Long userId) { userService.getUserById(userId); List itemsByOwner = itemRepository.findAllByOwnerId(userId); log.info("Получен список всех вещей, сдаваемых пользователем с ID {}", userId); - return itemsByOwner; + return itemsByOwner.stream() + .map(ItemMapper::toItemDto) + .toList(); } @Override @Transactional(readOnly = true) - public List search(String text) { + public List search(String text) { if (text == null || text.isBlank()) { log.info("Пустой запрос для поиска"); return List.of(); } - return itemRepository.search(text.toLowerCase()); + List items = itemRepository.search(text.toLowerCase()); + + return items.stream() + .map(ItemMapper::toItemDto) + .toList(); } @Override - public Comment addComment(Long userId, Long itemId, CommentDto dto) { - Item item = getItemById(itemId, userId); - User author = userService.getUserById(userId); + public CommentDto addComment(Long userId, Long itemId, NewCommentRequest newCommentRequest) { + Item item = getEntityById(itemId); + User author = userService.getEntityById(userId); // Проверяем есть ли хотя бы одно завершённое бронирование этой вещи этим пользователем boolean hasUsedItem = bookingRepository.existsByItemIdAndBookerIdAndEndBeforeAndStatus( - itemId, userId, LocalDateTime.now(), Booking.BookingStatus.APPROVED); + itemId, userId, LocalDateTime.now(), BookingStatus.APPROVED); if (!hasUsedItem) { throw new ValidationException("Пользователь не брал эту вещь или бронирование не завершено"); } Comment comment = Comment.builder() - .message(dto.getText()) + .message(newCommentRequest.getText()) .author(author) .item(item) .created(LocalDateTime.now()) .build(); + Comment saved = commentRepository.save(comment); - return commentRepository.save(comment); + return CommentMapper.toDto(saved); } } \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/item/ItemShortDto.java b/server/src/main/java/ru/practicum/shareit/item/ItemShortDto.java new file mode 100644 index 0000000..e08b503 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/item/ItemShortDto.java @@ -0,0 +1,17 @@ +package ru.practicum.shareit.item; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ItemShortDto { + private Long id; + private String name; + private Long requestId; + private Long ownerId; +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/comment/Comment.java b/server/src/main/java/ru/practicum/shareit/item/comment/Comment.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/comment/Comment.java rename to server/src/main/java/ru/practicum/shareit/item/comment/Comment.java diff --git a/src/main/java/ru/practicum/shareit/item/comment/CommentMapper.java b/server/src/main/java/ru/practicum/shareit/item/comment/CommentMapper.java similarity index 94% rename from src/main/java/ru/practicum/shareit/item/comment/CommentMapper.java rename to server/src/main/java/ru/practicum/shareit/item/comment/CommentMapper.java index b29d429..8a4b272 100644 --- a/src/main/java/ru/practicum/shareit/item/comment/CommentMapper.java +++ b/server/src/main/java/ru/practicum/shareit/item/comment/CommentMapper.java @@ -1,6 +1,7 @@ package ru.practicum.shareit.item.comment; +import ru.practicum.item.comment.CommentDto; import ru.practicum.shareit.item.Item; import ru.practicum.shareit.user.User; diff --git a/src/main/java/ru/practicum/shareit/item/comment/CommentRepository.java b/server/src/main/java/ru/practicum/shareit/item/comment/CommentRepository.java similarity index 100% rename from src/main/java/ru/practicum/shareit/item/comment/CommentRepository.java rename to server/src/main/java/ru/practicum/shareit/item/comment/CommentRepository.java diff --git a/server/src/main/java/ru/practicum/shareit/request/ItemRequest.java b/server/src/main/java/ru/practicum/shareit/request/ItemRequest.java new file mode 100644 index 0000000..a4743c7 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/ItemRequest.java @@ -0,0 +1,31 @@ +package ru.practicum.shareit.request; + +import jakarta.persistence.*; +import lombok.*; +import ru.practicum.shareit.user.User; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "item_requests") +public class ItemRequest { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "description", nullable = false) + private String description; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "requestor_id", nullable = false) + private User requester; + + @Column(name = "created", nullable = false) + private LocalDateTime created; +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/request/ItemRequestController.java b/server/src/main/java/ru/practicum/shareit/request/ItemRequestController.java new file mode 100644 index 0000000..a60a9bf --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/ItemRequestController.java @@ -0,0 +1,40 @@ +package ru.practicum.shareit.request; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.practicum.request.CreateItemRequestDto; +import ru.practicum.request.ItemRequestDto; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping(path = "/requests") +public class ItemRequestController { + + private final ItemRequestService itemRequestService; + + @PostMapping + public ItemRequestDto createRequest(@RequestHeader("X-Sharer-User-Id") Long userId, + @Valid @RequestBody CreateItemRequestDto createItemRequestDto) { + + return itemRequestService.createRequest(userId, createItemRequestDto); + } + + @GetMapping + public List getOwnRequests(@RequestHeader("X-Sharer-User-Id") Long userId) { + return itemRequestService.getOwnRequests(userId); + } + + @GetMapping("/all") + public List getAllRequests(@RequestHeader("X-Sharer-User-Id") Long userId) { + return itemRequestService.getAllRequests(userId); + } + + @GetMapping("/{requestId}") + public ItemRequestResponseDto getRequest(@RequestHeader("X-Sharer-User-Id") Long userId, + @PathVariable Long requestId) { + return itemRequestService.getRequestById(userId, requestId); + } +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/request/ItemRequestMapper.java b/server/src/main/java/ru/practicum/shareit/request/ItemRequestMapper.java new file mode 100644 index 0000000..a221586 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/ItemRequestMapper.java @@ -0,0 +1,16 @@ +package ru.practicum.shareit.request; + +import ru.practicum.request.ItemRequestDto; + +public class ItemRequestMapper { + + public static ItemRequestDto toDto(ItemRequest req) { + ItemRequestDto dto = new ItemRequestDto(); + + dto.setId(req.getId()); + dto.setDescription(req.getDescription()); + dto.setCreated(req.getCreated()); + + return dto; + } +} diff --git a/server/src/main/java/ru/practicum/shareit/request/ItemRequestRepository.java b/server/src/main/java/ru/practicum/shareit/request/ItemRequestRepository.java new file mode 100644 index 0000000..559ccfb --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/ItemRequestRepository.java @@ -0,0 +1,13 @@ +package ru.practicum.shareit.request; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ItemRequestRepository extends JpaRepository { + + List findAllByRequesterIdOrderByCreatedDesc(Long userId); + + List findAllByRequesterIdNot(Long userId); + +} diff --git a/server/src/main/java/ru/practicum/shareit/request/ItemRequestResponseDto.java b/server/src/main/java/ru/practicum/shareit/request/ItemRequestResponseDto.java new file mode 100644 index 0000000..5e0a35c --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/ItemRequestResponseDto.java @@ -0,0 +1,24 @@ +package ru.practicum.shareit.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.shareit.item.ItemShortDto; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ItemRequestResponseDto { + private Long id; + private String description; + private LocalDateTime created; + + @Builder.Default + private List items = new ArrayList<>(); +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/request/ItemRequestService.java b/server/src/main/java/ru/practicum/shareit/request/ItemRequestService.java new file mode 100644 index 0000000..44da33b --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/ItemRequestService.java @@ -0,0 +1,17 @@ +package ru.practicum.shareit.request; + +import ru.practicum.request.CreateItemRequestDto; +import ru.practicum.request.ItemRequestDto; + +import java.util.List; + +public interface ItemRequestService { + + ItemRequestDto createRequest(Long userId, CreateItemRequestDto createItemRequestDto); + + List getOwnRequests(Long userId); + + List getAllRequests(Long userId); + + ItemRequestResponseDto getRequestById(Long userId, Long requestId); +} \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/request/ItemRequestServiceImpl.java b/server/src/main/java/ru/practicum/shareit/request/ItemRequestServiceImpl.java new file mode 100644 index 0000000..a76cf9f --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/request/ItemRequestServiceImpl.java @@ -0,0 +1,107 @@ +package ru.practicum.shareit.request; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.request.CreateItemRequestDto; +import ru.practicum.request.ItemRequestDto; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.item.Item; +import ru.practicum.shareit.item.ItemRepository; +import ru.practicum.shareit.item.ItemShortDto; +import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.UserService; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional +public class ItemRequestServiceImpl implements ItemRequestService { + private final ItemRequestRepository itemRequestRepository; + private final UserService userService; + private final ItemRepository itemRepository; + + @Override + public ItemRequestDto createRequest(Long userId, CreateItemRequestDto createItemRequestDto) { + User user = userService.getEntityById(userId); + + ItemRequest request = new ItemRequest(); + request.setDescription(createItemRequestDto.getDescription()); + request.setRequester(user); + request.setCreated(LocalDateTime.now()); + + request = itemRequestRepository.save(request); + + return ItemRequestMapper.toDto(request); + } + + @Override + public List getOwnRequests(Long userId) { + List requests = itemRequestRepository.findAllByRequesterIdOrderByCreatedDesc(userId); + if (requests.isEmpty()) return List.of(); + + List requestIds = requests.stream() + .map(ItemRequest::getId) + .toList(); + + List items = itemRepository.findAllByRequestIdIn(requestIds); + + Map> itemsByRequest = items.stream() + .collect(Collectors.groupingBy( + Item::getRequestId, + Collectors.mapping( + item -> ItemShortDto.builder() + .id(item.getId()) + .name(item.getName()) + .ownerId(item.getOwner().getId()) + .build(), + Collectors.toList() + ) + )); + + return requests.stream() + .map(req -> ItemRequestResponseDto.builder() + .id(req.getId()) + .description(req.getDescription()) + .created(req.getCreated()) + .items(itemsByRequest.getOrDefault(req.getId(), List.of())) + .build()) + .collect(Collectors.toList()); + } + + @Override + public List getAllRequests(Long userId) { + return itemRequestRepository.findAllByRequesterIdNot(userId).stream() + .map(ItemRequestMapper::toDto) + .toList(); + } + + @Override + public ItemRequestResponseDto getRequestById(Long userId, Long requestId) { + ItemRequest request = itemRequestRepository.findById(requestId) + .orElseThrow(() -> new NotFoundException("Request not found")); + + List items = itemRepository.findAllByRequestId(requestId); + + List itemDtos = items.stream() + .map(item -> ItemShortDto.builder() + .id(item.getId()) + .name(item.getName()) + .ownerId(item.getOwner().getId()) + .requestId(item.getRequestId()) // если поле нужно + .build() + ) + .toList(); + + return ItemRequestResponseDto.builder() + .id(request.getId()) + .description(request.getDescription()) + .created(request.getCreated()) + .items(itemDtos) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/response/ErrorResponse.java b/server/src/main/java/ru/practicum/shareit/response/ErrorResponse.java similarity index 100% rename from src/main/java/ru/practicum/shareit/response/ErrorResponse.java rename to server/src/main/java/ru/practicum/shareit/response/ErrorResponse.java diff --git a/src/main/java/ru/practicum/shareit/user/User.java b/server/src/main/java/ru/practicum/shareit/user/User.java similarity index 100% rename from src/main/java/ru/practicum/shareit/user/User.java rename to server/src/main/java/ru/practicum/shareit/user/User.java diff --git a/src/main/java/ru/practicum/shareit/user/UserController.java b/server/src/main/java/ru/practicum/shareit/user/UserController.java similarity index 60% rename from src/main/java/ru/practicum/shareit/user/UserController.java rename to server/src/main/java/ru/practicum/shareit/user/UserController.java index 1cc6ab2..f4150eb 100644 --- a/src/main/java/ru/practicum/shareit/user/UserController.java +++ b/server/src/main/java/ru/practicum/shareit/user/UserController.java @@ -4,6 +4,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; +import ru.practicum.user.NewUserRequest; +import ru.practicum.user.UpdateUserRequest; +import ru.practicum.user.UserDto; import java.util.List; @@ -15,27 +18,25 @@ public class UserController { private final UserService userService; @PostMapping - public UserDto create(@Valid @RequestBody UserDto userDto) { - return UserMapper.toUserDto(userService.create(userDto)); + public UserDto create(@Valid @RequestBody NewUserRequest newUserRequest) { + return userService.create(newUserRequest); } @PatchMapping("/{id}") public UserDto update(@PathVariable Long id, - @RequestBody UserDto userDto) { - userDto.setId(id); - return UserMapper.toUserDto(userService.update(userDto)); + @Valid @RequestBody UpdateUserRequest updateUserRequest) { + updateUserRequest.setId(id); + return userService.update(updateUserRequest); } @GetMapping("/{id}") public UserDto getById(@PathVariable Long id) { - return UserMapper.toUserDto(userService.getUserById(id)); + return userService.getUserById(id); } @GetMapping public List getAll() { - return userService.getAll().stream() - .map(UserMapper::toUserDto) - .toList(); + return userService.getAll(); } @DeleteMapping("/{id}") diff --git a/server/src/main/java/ru/practicum/shareit/user/UserMapper.java b/server/src/main/java/ru/practicum/shareit/user/UserMapper.java new file mode 100644 index 0000000..bf567bb --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/UserMapper.java @@ -0,0 +1,36 @@ +package ru.practicum.shareit.user; + +import ru.practicum.user.NewUserRequest; +import ru.practicum.user.UpdateUserRequest; +import ru.practicum.user.UserDto; + +public class UserMapper { + + public static UserDto toUserDto(User user) { + return UserDto.builder() + .id(user.getId()) + .name(user.getName()) + .email(user.getEmail()) + .build(); + } + + public static User toUser(NewUserRequest newUserRequest) { + return User.builder() + .name(newUserRequest.getName()) + .email(newUserRequest.getEmail()) + .build(); + } + + public static User toUser(UserDto userDto) { + return User.builder() + .name(userDto.getName()) + .email(userDto.getEmail()) + .build(); + } + + public static void updateUserFields(User user, UpdateUserRequest updateUserRequest) { + if (updateUserRequest.getName() != null) { + user.setName(updateUserRequest.getName()); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/UserRepository.java b/server/src/main/java/ru/practicum/shareit/user/UserRepository.java similarity index 84% rename from src/main/java/ru/practicum/shareit/user/UserRepository.java rename to server/src/main/java/ru/practicum/shareit/user/UserRepository.java index 6f3696f..6036dab 100644 --- a/src/main/java/ru/practicum/shareit/user/UserRepository.java +++ b/server/src/main/java/ru/practicum/shareit/user/UserRepository.java @@ -5,4 +5,6 @@ public interface UserRepository extends JpaRepository { boolean existsByEmail(String email); + + boolean existsById(Long userId); } \ No newline at end of file diff --git a/server/src/main/java/ru/practicum/shareit/user/UserService.java b/server/src/main/java/ru/practicum/shareit/user/UserService.java new file mode 100644 index 0000000..ef57668 --- /dev/null +++ b/server/src/main/java/ru/practicum/shareit/user/UserService.java @@ -0,0 +1,22 @@ +package ru.practicum.shareit.user; + +import ru.practicum.user.NewUserRequest; +import ru.practicum.user.UpdateUserRequest; +import ru.practicum.user.UserDto; + +import java.util.List; + +public interface UserService { + + UserDto create(NewUserRequest newUserRequest); + + UserDto update(UpdateUserRequest updateUserRequest); + + UserDto getUserById(Long id); + + User getEntityById(Long id); + + List getAll(); + + void delete(Long id); +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java b/server/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java similarity index 55% rename from src/main/java/ru/practicum/shareit/user/UserServiceImpl.java rename to server/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java index 0ed417f..46d4197 100644 --- a/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java +++ b/server/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java @@ -7,6 +7,9 @@ import ru.practicum.shareit.exception.ConflictException; import ru.practicum.shareit.exception.NotFoundException; import ru.practicum.shareit.exception.ValidationException; +import ru.practicum.user.NewUserRequest; +import ru.practicum.user.UpdateUserRequest; +import ru.practicum.user.UserDto; import java.util.*; @@ -18,63 +21,73 @@ public class UserServiceImpl implements UserService { private final UserRepository userRepository; @Override - public User create(UserDto userDto) { - if (userRepository.existsByEmail(userDto.getEmail())) { - log.error("Ошибка создания пользователя: email {} уже используется", userDto.getEmail()); + public UserDto create(NewUserRequest newUserRequest) { + if (userRepository.existsByEmail(newUserRequest.getEmail())) { + log.error("Ошибка создания пользователя: email {} уже используется", newUserRequest.getEmail()); throw new ConflictException("Данный email уже используется"); } - User user = UserMapper.toUser(userDto); + User user = UserMapper.toUser(newUserRequest); User createdUser = userRepository.save(user); log.info("Пользователь успешно создан с id: {}, email: {}", user.getId(), user.getEmail()); - return createdUser; + return UserMapper.toUserDto(createdUser); } @Override - public User update(UserDto userDto) { - if (userDto.getId() == null || userDto.getId() == 0) { + public UserDto update(UpdateUserRequest updateUserRequest) { + if (updateUserRequest.getId() == null || updateUserRequest.getId() == 0) { throw new ValidationException("id должен быть указан"); } - User existingUser = userRepository.findById(userDto.getId()) + User existingUser = userRepository.findById(updateUserRequest.getId()) .orElseThrow(() -> new NotFoundException("Пользователь не найден")); - if (userDto.getEmail() != null && !Objects.equals(userDto.getEmail(), existingUser.getEmail())) { - if (userRepository.existsByEmail(userDto.getEmail())) { - log.error("Ошибка обновления: email {} уже используется", userDto.getEmail()); + if (updateUserRequest.getEmail() != null && !Objects.equals(updateUserRequest.getEmail(), existingUser.getEmail())) { + if (userRepository.existsByEmail(updateUserRequest.getEmail())) { + log.error("Ошибка обновления: email {} уже используется", updateUserRequest.getEmail()); throw new ConflictException("Этот e-mail уже используется"); } - existingUser.setEmail(userDto.getEmail()); + existingUser.setEmail(updateUserRequest.getEmail()); } - UserMapper.updateUserFields(existingUser, userDto); + UserMapper.updateUserFields(existingUser, updateUserRequest); User updatedUser = userRepository.save(existingUser); log.info("Пользователь с id {} успешно обновлён", updatedUser.getId()); - return updatedUser; + return UserMapper.toUserDto(updatedUser); } @Override @Transactional(readOnly = true) - public User getUserById(Long userId) { - return userRepository.findById(userId) + public UserDto getUserById(Long userId) { + User user = userRepository.findById(userId) .orElseThrow(() -> { log.error("Пользователь с id {} не найден", userId); return new NotFoundException("Пользователь с id " + userId + " не найден."); }); + return UserMapper.toUserDto(user); + } + + @Override + public User getEntityById(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> + new NotFoundException("Пользователь с id=" + id + " не найден")); } @Override @Transactional(readOnly = true) - public List getAll() { - return userRepository.findAll(); + public List getAll() { + return userRepository.findAll().stream() + .map(UserMapper::toUserDto) + .toList(); } @Override public void delete(Long userId) { - User user = getUserById(userId); + getEntityById(userId); userRepository.deleteById(userId); log.info("Пользователь с id {} удалён", userId); } diff --git a/src/main/resources/application-test.properties b/server/src/main/resources/application-test.properties similarity index 65% rename from src/main/resources/application-test.properties rename to server/src/main/resources/application-test.properties index 16ae096..3d6ed0c 100644 --- a/src/main/resources/application-test.properties +++ b/server/src/main/resources/application-test.properties @@ -3,8 +3,10 @@ spring.datasource.driver-class-name=org.h2.Driver spring.datasource.username=sa spring.datasource.password= -spring.jpa.hibernate.ddl-auto=none +#spring.jpa.hibernate.ddl-auto=none spring.sql.init.mode=always spring.jpa.show-sql=true -spring.jpa.properties.hibernate.format_sql=true \ No newline at end of file +spring.jpa.properties.hibernate.format_sql=true + +spring.jpa.hibernate.ddl-auto=create-drop diff --git a/src/main/resources/application.properties b/server/src/main/resources/application.properties similarity index 84% rename from src/main/resources/application.properties rename to server/src/main/resources/application.properties index aef7a50..87229d8 100644 --- a/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -1,15 +1,19 @@ + +server.port=9090 + logging.level.org.springframework.orm.jpa=INFO logging.level.org.springframework.transaction=INFO logging.level.org.springframework.transaction.interceptor=TRACE logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG -# TODO Append connection to DB spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://localhost:5432/shareIt spring.datasource.username=dbuser spring.datasource.password=12345 spring.sql.init.mode=always -spring.jpa.hibernate.ddl-auto=none +#spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=update + spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true \ No newline at end of file diff --git a/src/main/resources/schema.sql b/server/src/main/resources/schema.sql similarity index 75% rename from src/main/resources/schema.sql rename to server/src/main/resources/schema.sql index a7a038d..2c648fd 100644 --- a/src/main/resources/schema.sql +++ b/server/src/main/resources/schema.sql @@ -6,6 +6,15 @@ CREATE TABLE IF NOT EXISTS users ( CONSTRAINT UQ_USER_EMAIL UNIQUE (email) ); +CREATE TABLE IF NOT EXISTS item_requests ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + description TEXT NOT NULL, + requestor_id BIGINT NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + CONSTRAINT pk_item_request PRIMARY KEY (id), + CONSTRAINT fk_item_request_user FOREIGN KEY (requestor_id) REFERENCES users(id) ON DELETE CASCADE +); + CREATE TABLE IF NOT EXISTS items ( id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, @@ -14,7 +23,8 @@ CREATE TABLE IF NOT EXISTS items ( owner_id BIGINT NOT NULL, request_id BIGINT, CONSTRAINT pk_item PRIMARY KEY (id), - CONSTRAINT fk_item_owner FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE + CONSTRAINT fk_item_owner FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT fk_item_request FOREIGN KEY (request_id) REFERENCES item_requests(id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS bookings ( diff --git a/src/test/java/ru/practicum/shareit/ShareItTests.java b/server/src/test/java/ru/practicum/shareit/ShareItTests.java similarity index 71% rename from src/test/java/ru/practicum/shareit/ShareItTests.java rename to server/src/test/java/ru/practicum/shareit/ShareItTests.java index 4d79052..b1e33ea 100644 --- a/src/test/java/ru/practicum/shareit/ShareItTests.java +++ b/server/src/test/java/ru/practicum/shareit/ShareItTests.java @@ -2,12 +2,13 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; @SpringBootTest +@ActiveProfiles("test") class ShareItTests { @Test void contextLoads() { } - -} +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java new file mode 100644 index 0000000..b67b72a --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingControllerTest.java @@ -0,0 +1,108 @@ +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.booking.BookingDto; +import ru.practicum.booking.NewBookingRequestDto; +import ru.practicum.shareit.item.Item; +import ru.practicum.shareit.item.ItemRepository; +import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.UserRepository; + +import java.time.LocalDateTime; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +class BookingControllerTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private ObjectMapper mapper; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ItemRepository itemRepository; + + @Autowired + private BookingRepository bookingRepository; + + private User owner; + private User booker; + private Item item; + + @BeforeEach + void setup() { + bookingRepository.deleteAll(); + itemRepository.deleteAll(); + userRepository.deleteAll(); + + owner = userRepository.save(new User(null, "owner@mail.com", "Owner")); + booker = userRepository.save(new User(null, "booker@mail.com", "Booker")); + + item = itemRepository.save(Item.builder() + .name("Drill") + .description("Power tool") + .available(true) + .owner(owner) + .build()); + } + + @Test + void createAndApproveAndGetBooking() throws Exception { + NewBookingRequestDto bookingRequest = NewBookingRequestDto.builder() + .itemId(item.getId()) + .start(LocalDateTime.now().plusMinutes(1)) + .end(LocalDateTime.now().plusHours(1)) + .build(); + + String bookingJson = mvc.perform(post("/bookings") + .header("X-Sharer-User-Id", booker.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(bookingRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.item.id", is(item.getId().intValue()))) + .andExpect(jsonPath("$.booker.id", is(booker.getId().intValue()))) + .andExpect(jsonPath("$.status", is("WAITING"))) + .andReturn() + .getResponse() + .getContentAsString(); + + BookingDto bookingDto = mapper.readValue(bookingJson, BookingDto.class); + + mvc.perform(patch("/bookings/" + bookingDto.getId() + "?approved=true") + .header("X-Sharer-User-Id", owner.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status", is("APPROVED"))); + + mvc.perform(get("/bookings/" + bookingDto.getId()) + .header("X-Sharer-User-Id", booker.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(bookingDto.getId().intValue()))) + .andExpect(jsonPath("$.status", is("APPROVED"))); + + mvc.perform(get("/bookings") + .header("X-Sharer-User-Id", booker.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))); + + mvc.perform(get("/bookings/owner") + .header("X-Sharer-User-Id", owner.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingDtoJsonTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingDtoJsonTest.java new file mode 100644 index 0000000..cd6bd27 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingDtoJsonTest.java @@ -0,0 +1,61 @@ +package ru.practicum.shareit.booking; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import ru.practicum.booking.BookingDto; +import ru.practicum.booking.BookingStatus; +import ru.practicum.item.ItemDto; +import ru.practicum.user.UserDto; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.*; + +@JsonTest +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class BookingDtoJsonTest { + + private final JacksonTester json; + + @Test + void testBookingDto() throws Exception { + ItemDto item = ItemDto.builder() + .id(2L) + .name("bar") + .build(); + + UserDto booker = UserDto.builder() + .id(5L) + .name("A") + .email("a@a.com") + .build(); + + BookingDto dto = BookingDto.builder() + .id(10L) + .item(item) + .booker(booker) + .start(LocalDateTime.of(2025, 6, 16, 12, 0)) + .end(LocalDateTime.of(2025, 6, 17, 12, 0)) + .status(BookingStatus.APPROVED) + .build(); + + JsonContent result = json.write(dto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(10); + assertThat(result).extractingJsonPathNumberValue("$.item.id").isEqualTo(2); + assertThat(result).extractingJsonPathStringValue("$.item.name").isEqualTo("bar"); + assertThat(result).extractingJsonPathNumberValue("$.booker.id").isEqualTo(5); + assertThat(result).extractingJsonPathStringValue("$.booker.name").isEqualTo("A"); + assertThat(result).extractingJsonPathStringValue("$.booker.email").isEqualTo("a@a.com"); + assertThat(result).extractingJsonPathStringValue("$.start") + .isEqualTo("2025-06-16T12:00:00"); + assertThat(result).extractingJsonPathStringValue("$.end") + .isEqualTo("2025-06-17T12:00:00"); + assertThat(result).extractingJsonPathStringValue("$.status") + .isEqualTo("APPROVED"); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingMapperTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingMapperTest.java new file mode 100644 index 0000000..6b94360 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingMapperTest.java @@ -0,0 +1,46 @@ +package ru.practicum.shareit.booking; + +import org.junit.jupiter.api.Test; +import ru.practicum.booking.BookingDto; +import ru.practicum.booking.NewBookingRequestDto; +import ru.practicum.shareit.item.Item; +import ru.practicum.shareit.user.User; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.*; + +class BookingMapperTest { + + @Test + void toAndFromDto() { + LocalDateTime start = LocalDateTime.now(); + LocalDateTime end = start.plusDays(1); + + NewBookingRequestDto req = new NewBookingRequestDto(); + req.setItemId(3L); + req.setStart(start); + req.setEnd(end); + + User booker = new User(2L, "u@u.com", "U"); + Item item = Item.builder() + .id(3L) + .name("drill") + .description("tool") + .available(true) + .owner(booker) + .build(); + + Booking domain = BookingMapper.toBooking(req, item, booker); + assertThat(domain.getItem()).isEqualTo(item); + assertThat(domain.getBooker()).isEqualTo(booker); + assertThat(domain.getStart()).isEqualTo(start); + assertThat(domain.getEnd()).isEqualTo(end); + + BookingDto dto = BookingMapper.toDto(domain); + assertThat(dto.getItem().getId()).isEqualTo(item.getId()); + assertThat(dto.getBooker().getId()).isEqualTo(booker.getId()); + assertThat(dto.getStart()).isEqualTo(start); + assertThat(dto.getEnd()).isEqualTo(end); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingRequestDtoJsonTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingRequestDtoJsonTest.java new file mode 100644 index 0000000..28c8b26 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingRequestDtoJsonTest.java @@ -0,0 +1,52 @@ +package ru.practicum.shareit.booking; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import ru.practicum.booking.NewBookingRequestDto; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@JsonTest +class BookingRequestDtoJsonTest { + + @Autowired + private JacksonTester json; + + @Test + void testSerialize() throws Exception { + NewBookingRequestDto dto = new NewBookingRequestDto(); + dto.setItemId(5L); + dto.setStart(LocalDateTime.of(2025, 6, 16, 8, 30)); + dto.setEnd(LocalDateTime.of(2025, 6, 17, 9, 45)); + + JsonContent content = json.write(dto); + + assertThat(content).hasJsonPathNumberValue("$.itemId"); + assertThat(content).extractingJsonPathNumberValue("$.itemId").isEqualTo(5); + assertThat(content).extractingJsonPathStringValue("$.start") + .isEqualTo("2025-06-16T08:30:00"); + assertThat(content).extractingJsonPathStringValue("$.end") + .isEqualTo("2025-06-17T09:45:00"); + } + + @Test + void testRoundTripSerializeDeserialize() throws Exception { + NewBookingRequestDto original = new NewBookingRequestDto(); + original.setItemId(5L); + original.setStart(LocalDateTime.of(2025, 6, 16, 8, 30)); + original.setEnd(LocalDateTime.of(2025, 6, 17, 9, 45)); + + String jsonString = json.write(original).getJson(); + + NewBookingRequestDto parsed = json.parseObject(jsonString); + + assertThat(parsed.getItemId()).isEqualTo(original.getItemId()); + assertThat(parsed.getStart()).isEqualTo(original.getStart()); + assertThat(parsed.getEnd()).isEqualTo(original.getEnd()); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingServiceImplTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingServiceImplTest.java new file mode 100644 index 0000000..56123d4 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingServiceImplTest.java @@ -0,0 +1,155 @@ +package ru.practicum.shareit.booking; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; +import ru.practicum.booking.BookingDto; +import ru.practicum.booking.BookingStatus; +import ru.practicum.booking.NewBookingRequestDto; +import ru.practicum.shareit.exception.ConflictException; +import ru.practicum.shareit.exception.ForbiddenException; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.exception.ValidationException; +import ru.practicum.shareit.item.Item; +import ru.practicum.shareit.item.ItemRepository; +import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.UserService; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class BookingServiceImplTest { + + @InjectMocks + private BookingServiceImpl bookingService; + + @Mock + private BookingRepository bookingRepository; + + @Mock + private ItemRepository itemRepository; + + @Mock + private UserService userService; + + @Captor + private ArgumentCaptor bookingCaptor; + + private User owner; + private User booker; + private Item item; + private Booking booking; + private NewBookingRequestDto bookingRequest; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + + owner = new User(); + owner.setId(1L); + + booker = new User(); + booker.setId(2L); + + item = new Item(); + item.setId(1L); + item.setAvailable(true); + item.setOwner(owner); + + bookingRequest = new NewBookingRequestDto(); + bookingRequest.setItemId(item.getId()); + bookingRequest.setStart(LocalDateTime.now().plusDays(1)); + bookingRequest.setEnd(LocalDateTime.now().plusDays(2)); + + booking = new Booking(); + booking.setId(1L); + booking.setItem(item); + booking.setBooker(booker); + booking.setStatus(BookingStatus.WAITING); + booking.setStart(bookingRequest.getStart()); + booking.setEnd(bookingRequest.getEnd()); + } + + @Test + void createBooking_shouldCreateBookingSuccessfully() { + when(userService.getEntityById(anyLong())).thenReturn(booker); + when(itemRepository.findById(anyLong())).thenReturn(Optional.of(item)); + when(bookingRepository.findOverlappingBookings(anyLong(), any(), any())).thenReturn(List.of()); + when(bookingRepository.save(any())).thenReturn(booking); + + BookingDto result = bookingService.createBooking(booker.getId(), bookingRequest); + + assertNotNull(result); + verify(bookingRepository).save(bookingCaptor.capture()); + assertEquals(BookingStatus.WAITING, bookingCaptor.getValue().getStatus()); + } + + @Test + void createBooking_shouldThrowIfItemUnavailable() { + item.setAvailable(false); + when(userService.getEntityById(anyLong())).thenReturn(booker); + when(itemRepository.findById(anyLong())).thenReturn(Optional.of(item)); + + assertThrows(ValidationException.class, () -> bookingService.createBooking(booker.getId(), bookingRequest)); + } + + @Test + void createBooking_shouldThrowIfOverlapping() { + when(userService.getEntityById(anyLong())).thenReturn(booker); + when(itemRepository.findById(anyLong())).thenReturn(Optional.of(item)); + when(bookingRepository.findOverlappingBookings(anyLong(), any(), any())) + .thenReturn(List.of(new Booking())); + + assertThrows(ValidationException.class, () -> bookingService.createBooking(booker.getId(), bookingRequest)); + } + + @Test + void approveBooking_shouldApproveSuccessfully() { + booking.setStatus(BookingStatus.WAITING); + when(bookingRepository.findById(anyLong())).thenReturn(Optional.of(booking)); + booking.setItem(item); + + BookingDto result = bookingService.approveBooking(owner.getId(), booking.getId(), true); + + assertEquals(BookingStatus.APPROVED, booking.getStatus()); + verify(bookingRepository).save(booking); + } + + @Test + void approveBooking_shouldThrowForbiddenException() { + booking.setStatus(BookingStatus.WAITING); + when(bookingRepository.findById(anyLong())).thenReturn(Optional.of(booking)); + + assertThrows(ForbiddenException.class, () -> bookingService.approveBooking( + 99L, booking.getId(), true)); + } + + @Test + void approveBooking_shouldThrowConflictException() { + booking.setStatus(BookingStatus.APPROVED); + when(bookingRepository.findById(anyLong())).thenReturn(Optional.of(booking)); + + assertThrows(ConflictException.class, () -> bookingService.approveBooking( + owner.getId(), booking.getId(), true)); + } + + @Test + void getBookingById_shouldReturnBookingIfOwnerOrBooker() { + when(bookingRepository.findById(anyLong())).thenReturn(Optional.of(booking)); + booking.setItem(item); + + assertNotNull(bookingService.getBookingById(owner.getId(), booking.getId())); + assertNotNull(bookingService.getBookingById(booker.getId(), booking.getId())); + } + + @Test + void getBookingById_shouldThrowNotFoundIfUnauthorized() { + when(bookingRepository.findById(anyLong())).thenReturn(Optional.of(booking)); + + assertThrows(NotFoundException.class, () -> bookingService.getBookingById(99L, booking.getId())); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/booking/BookingTest.java b/server/src/test/java/ru/practicum/shareit/booking/BookingTest.java new file mode 100644 index 0000000..cc917b7 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/booking/BookingTest.java @@ -0,0 +1,50 @@ +package ru.practicum.shareit.booking; + +import org.junit.jupiter.api.Test; +import ru.practicum.shareit.item.Item; +import ru.practicum.shareit.user.User; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +class BookingTest { + + @Test + void testEqualsAndHashCode() { + User u = new User(); + Item i = new Item(); + LocalDateTime now = LocalDateTime.now(); + + Booking b1 = Booking.builder() + .id(1L) + .item(i) + .booker(u) + .start(now) + .end(now.plusHours(1)) + .build(); + + Booking b2 = Booking.builder() + .id(1L) + .item(i) + .booker(u) + .start(now) + .end(now.plusHours(1)) + .build(); + + Booking b3 = Booking.builder() + .id(2L) + .build(); + + assertThat(b1).isEqualTo(b2); + assertThat(b1).hasSameHashCodeAs(b2); + assertThat(b1).isNotEqualTo(b3); + } + + @Test + void testGettersAndSetters() { + Booking b = new Booking(); + b.setId(1L); + assertThat(b.getId()).isEqualTo(1L); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/exception/ErrorHandlerTest.java b/server/src/test/java/ru/practicum/shareit/exception/ErrorHandlerTest.java new file mode 100644 index 0000000..79c07aa --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/exception/ErrorHandlerTest.java @@ -0,0 +1,33 @@ +package ru.practicum.shareit.exception; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; +import org.xml.sax.ErrorHandler; +import ru.practicum.shareit.booking.BookingService; +import ru.practicum.shareit.booking.BookingController; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest({BookingController.class, ErrorHandler.class}) +class ErrorHandlerTest { + + @MockBean + private BookingService bookingService; + @Autowired + private MockMvc mvc; + + @Test + void whenServiceThrowsNotFound_thenControllerReturns404() throws Exception { + when(bookingService.getBookingById(anyLong(), anyLong())) + .thenThrow(new NotFoundException("no such")); + + mvc.perform(get("/bookings/1").header("X-Sharer-User-Id", 1)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value("no such")); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandlerTest.java b/server/src/test/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandlerTest.java new file mode 100644 index 0000000..7f64af4 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandlerTest.java @@ -0,0 +1,120 @@ +package ru.practicum.shareit.exception.handler; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.booking.BookingStatus; +import ru.practicum.shareit.booking.Booking; +import ru.practicum.shareit.booking.BookingRepository; +import ru.practicum.shareit.item.Item; +import ru.practicum.shareit.item.ItemRepository; +import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.UserRepository; + +import java.time.LocalDateTime; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +class GlobalExceptionHandlerTest { + + @Autowired + private MockMvc mvc; + + @Autowired + UserRepository userRepository; + + @Autowired + ItemRepository itemRepository; + + @Autowired + BookingRepository bookingRepository; + + @Test + void handleNotFound() throws Exception { + mvc.perform(get("/users/99999")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value(404)) + .andExpect(jsonPath("$.error").value("Not Found")) + .andExpect(jsonPath("$.message", containsString("не найден"))) + .andExpect(jsonPath("$.path").value("/users/99999")); + } + + @Test + void handleValidation() throws Exception { + // Некорректный email => валидация должна сработать + mvc.perform( + post("/users") + .contentType("application/json") + .content("{\"name\": \"test\", \"email\": \"not-an-email\"}") + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.error").value("Bad Request")) + .andExpect(jsonPath("$.message", containsString("email: Некорректный email"))) + .andExpect(jsonPath("$.path").value("/users")); + } + + @Test + void handleConflict() throws Exception { + String userJson = "{\"name\": \"Test\", \"email\": \"test@test.com\"}"; + mvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(userJson)) + .andExpect(status().isOk()); + + mvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(userJson)) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.status").value(409)) + .andExpect(jsonPath("$.error").value("Conflict")) + .andExpect(jsonPath("$.message", containsString("Данный email уже используется"))) + .andExpect(jsonPath("$.path").value("/users")); + } + + @Test + void handleForbidden() throws Exception { + User owner = userRepository.save(new User(null, "owner1@mail.com", "Owner")); + User other = userRepository.save(new User(null, "other@mail.com", "Other")); + + Item item = itemRepository.save(Item.builder() + .name("Drill") + .description("Power tool") + .available(true) + .owner(owner) + .build() + ); + + Booking booking = bookingRepository.save(Booking.builder() + .item(item) + .booker(other) + .start(LocalDateTime.now().plusDays(1)) + .end(LocalDateTime.now().plusDays(2)) + .status(BookingStatus.WAITING) + .build() + ); + + mvc.perform(patch("/bookings/" + booking.getId() + "?approved=true") + .header("X-Sharer-User-Id", other.getId())) + .andExpect(status().isForbidden()); + } + + @Test + void handleInternalServerError() throws Exception { + mvc.perform(get("/exception-test")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.status").value(500)) + .andExpect(jsonPath("$.error").value("Internal Server Error")) + .andExpect(jsonPath("$.message", containsString("Произошла ошибка"))) + .andExpect(jsonPath("$.path").value("/exception-test")); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemControllerTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemControllerTest.java new file mode 100644 index 0000000..d01a0e3 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/ItemControllerTest.java @@ -0,0 +1,174 @@ +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.booking.BookingStatus; +import ru.practicum.item.NewItemRequest; +import ru.practicum.item.UpdateItemRequest; +import ru.practicum.item.comment.NewCommentRequest; +import ru.practicum.shareit.booking.Booking; +import ru.practicum.shareit.booking.BookingRepository; +import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.UserRepository; + +import java.time.LocalDateTime; + +import static org.hamcrest.Matchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +class ItemControllerTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private ObjectMapper mapper; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ItemRepository itemRepository; + + @Autowired + private BookingRepository bookingRepository; + + private Long userId; + + @BeforeEach + void setUp() { + User user = userRepository.save(new User(null, "test@example.com", "Test User")); + userId = user.getId(); + } + + @Test + void createItem() throws Exception { + NewItemRequest newItemRequest = new NewItemRequest(); + newItemRequest.setName("Drill"); + newItemRequest.setDescription("Powerful drill"); + newItemRequest.setAvailable(true); + + mvc.perform(post("/items") + .header("X-Sharer-User-Id", userId) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(newItemRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", notNullValue())) + .andExpect(jsonPath("$.name").value("Drill")); + } + + @Test + void updateItem() throws Exception { + Item item = itemRepository.save(Item.builder() + .name("Old Name") + .description("Old Desc") + .available(true) + .owner(userRepository.findById(userId).get()) + .build()); + + UpdateItemRequest updateRequest = new UpdateItemRequest(); + updateRequest.setName("New Name"); + + mvc.perform(patch("/items/" + item.getId()) + .header("X-Sharer-User-Id", userId) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(updateRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("New Name")); + } + + @Test + void getById() throws Exception { + Item item = itemRepository.save(Item.builder() + .name("Hammer") + .description("For nails") + .available(true) + .owner(userRepository.findById(userId).get()) + .build()); + + mvc.perform(get("/items/" + item.getId()) + .header("X-Sharer-User-Id", userId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("Hammer")); + } + + @Test + void getAllByOwner() throws Exception { + itemRepository.save(Item.builder() + .name("Item1") + .description("Desc1") + .available(true) + .owner(userRepository.findById(userId).get()) + .build()); + + itemRepository.save(Item.builder() + .name("Item2") + .description("Desc2") + .available(true) + .owner(userRepository.findById(userId).get()) + .build()); + + mvc.perform(get("/items") + .header("X-Sharer-User-Id", userId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))); + } + + @Test + void searchItems() throws Exception { + itemRepository.save(Item.builder() + .name("Screwdriver") + .description("For screws") + .available(true) + .owner(userRepository.findById(userId).get()) + .build()); + + mvc.perform(get("/items/search") + .param("text", "screw")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name", containsString("Screw"))); + } + + @Test + void addComment() throws Exception { + User owner = userRepository.findById(userId).get(); + + User booker = userRepository.save(new User(null, "test@test.com", "Test User")); + + Item item = itemRepository.save(Item.builder() + .name("Drill") + .description("Power tool") + .available(true) + .owner(owner) + .build()); + + bookingRepository.save(Booking.builder() + .item(item) + .booker(booker) + .start(LocalDateTime.now().minusDays(3)) + .end(LocalDateTime.now().minusDays(1)) + .status(BookingStatus.APPROVED) + .build()); + + NewCommentRequest commentRequest = new NewCommentRequest(); + commentRequest.setText("Great tool!"); + + mvc.perform(post("/items/" + item.getId() + "/comment") + .header("X-Sharer-User-Id", booker.getId()) // ключевой момент! + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(commentRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.text").value("Great tool!")); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemDtoJsonTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemDtoJsonTest.java new file mode 100644 index 0000000..80f0525 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/ItemDtoJsonTest.java @@ -0,0 +1,26 @@ +package ru.practicum.shareit.item; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.json.JacksonTester; +import ru.practicum.item.ItemDto; + +import static org.assertj.core.api.Assertions.*; + +@JsonTest +class ItemDtoJsonTest { + + @Autowired + private JacksonTester json; + + @Test + void serializeDeserialize() throws Exception { + ItemDto dto = ItemDto.builder() + .id(5L).name("drill").description("tool").available(true).build(); + + String j = json.write(dto).getJson(); + assertThat(j).contains("\"name\":\"drill\""); + assertThat(json.parseObject(j).getAvailable()).isTrue(); + } +} \ No newline at end of file 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..c7d44e1 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/ItemMapperTest.java @@ -0,0 +1,86 @@ +package ru.practicum.shareit.item; + +import org.junit.jupiter.api.Test; +import ru.practicum.item.ItemDto; +import ru.practicum.item.NewItemRequest; +import ru.practicum.item.UpdateItemRequest; +import ru.practicum.shareit.user.User; + +import static org.assertj.core.api.Assertions.*; + +class ItemMapperTest { + + @Test + void toItemDto_shouldMapFields() { + Item item = Item.builder() + .id(1L) + .name("Drill") + .description("Powerful") + .available(true) + .requestId(5L) + .build(); + + ItemDto dto = ItemMapper.toItemDto(item); + + assertThat(dto.getId()).isEqualTo(1L); + assertThat(dto.getName()).isEqualTo("Drill"); + assertThat(dto.getDescription()).isEqualTo("Powerful"); + assertThat(dto.getAvailable()).isTrue(); + assertThat(dto.getRequestId()).isEqualTo(5L); + } + + @Test + void toShortItemDto_shouldMapFields() { + Item item = Item.builder() + .id(2L) + .name("Saw") + .requestId(7L) + .build(); + + ItemShortDto shortDto = ItemMapper.toShortItemDto(item); + + assertThat(shortDto.getId()).isEqualTo(2L); + assertThat(shortDto.getName()).isEqualTo("Saw"); + assertThat(shortDto.getRequestId()).isEqualTo(7L); + } + + @Test + void toItem_shouldMapNewItemRequest() { + NewItemRequest request = NewItemRequest.builder() + .name("Hammer") + .description("Metal hammer") + .available(true) + .build(); + + User owner = User.builder().id(1L).name("Alex").build(); + + Item item = ItemMapper.toItem(request, owner, 10L); + + assertThat(item.getName()).isEqualTo("Hammer"); + assertThat(item.getDescription()).isEqualTo("Metal hammer"); + assertThat(item.getAvailable()).isTrue(); + assertThat(item.getOwner()).isEqualTo(owner); + assertThat(item.getRequestId()).isEqualTo(10L); + } + + @Test + void updateItemFields_shouldUpdateOnlyNotNullFields() { + Item item = Item.builder() + .name("Old name") + .description("Old desc") + .available(true) + .build(); + + UpdateItemRequest update = UpdateItemRequest.builder() + .name("New name") + .description(null) // should not update + .available(false) + .build(); + + ItemMapper.updateItemFields(item, update); + + assertThat(item.getName()).isEqualTo("New name"); + assertThat(item.getDescription()).isEqualTo("Old desc"); + assertThat(item.getAvailable()).isFalse(); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemServiceImplTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemServiceImplTest.java new file mode 100644 index 0000000..da2acb9 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/ItemServiceImplTest.java @@ -0,0 +1,217 @@ +package ru.practicum.shareit.item; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; +import ru.practicum.booking.BookingStatus; +import ru.practicum.item.ItemDto; +import ru.practicum.item.NewItemRequest; +import ru.practicum.item.UpdateItemRequest; +import ru.practicum.item.comment.CommentDto; +import ru.practicum.item.comment.NewCommentRequest; +import ru.practicum.shareit.booking.BookingRepository; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.exception.ValidationException; +import ru.practicum.shareit.item.comment.Comment; +import ru.practicum.shareit.item.comment.CommentRepository; +import ru.practicum.shareit.request.ItemRequestRepository; +import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.UserService; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class ItemServiceImplTest { + + @InjectMocks + private ItemServiceImpl itemService; + + @Mock + private UserService userService; + + @Mock + private ItemRepository itemRepository; + + @Mock + private ItemRequestRepository itemRequestRepository; + + @Mock + private BookingRepository bookingRepository; + + @Mock + private CommentRepository commentRepository; + + private User owner; + private Item item; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + + owner = new User(); + owner.setId(1L); + + item = new Item(); + item.setId(1L); + item.setOwner(owner); + item.setAvailable(true); + item.setName("Item1"); + item.setDescription("Description1"); + } + + @Test + void create_shouldCreateItemWithRequestId() { + NewItemRequest request = new NewItemRequest(); + request.setName("Name"); + request.setDescription("Desc"); + request.setAvailable(true); + request.setRequestId(42L); + + when(userService.getEntityById(anyLong())).thenReturn(owner); + when(itemRequestRepository.existsById(anyLong())).thenReturn(true); + when(itemRepository.save(any())).thenReturn(item); + + ItemDto result = itemService.create(owner.getId(), request); + + assertNotNull(result); + verify(itemRepository).save(any()); + } + + @Test + void create_shouldThrowIfRequestIdNotFound() { + NewItemRequest request = new NewItemRequest(); + request.setRequestId(42L); + + when(userService.getEntityById(anyLong())).thenReturn(owner); + when(itemRequestRepository.existsById(anyLong())).thenReturn(false); + + assertThrows(NotFoundException.class, () -> itemService.create(owner.getId(), request)); + } + + @Test + void update_shouldUpdateItemFields() { + UpdateItemRequest update = new UpdateItemRequest(); + update.setName("New Name"); + + when(itemRepository.findById(anyLong())).thenReturn(Optional.of(item)); + + ItemDto result = itemService.update(owner.getId(), item.getId(), update); + + assertNotNull(result); + verify(itemRepository).save(item); + } + + @Test + void update_shouldThrowIfNotOwner() { + when(itemRepository.findById(anyLong())).thenReturn(Optional.of(item)); + + assertThrows(NotFoundException.class, () -> itemService.update(999L, item.getId(), new UpdateItemRequest())); + } + + @Test + void getItemById_shouldIncludeBookingsIfOwner() { + when(itemRepository.findById(anyLong())).thenReturn(Optional.of(item)); + when(commentRepository.findByItemIdOrderByCreatedDesc(anyLong())).thenReturn(List.of()); + + ItemDto result = itemService.getItemById(item.getId(), owner.getId()); + + assertNotNull(result); + verify(bookingRepository).findLastBooking(anyLong(), any()); + verify(bookingRepository).findNextBooking(anyLong(), any()); + } + + @Test + void getItemById_shouldNotIncludeBookingsIfNotOwner() { + when(itemRepository.findById(anyLong())).thenReturn(Optional.of(item)); + when(commentRepository.findByItemIdOrderByCreatedDesc(anyLong())).thenReturn(List.of()); + + ItemDto result = itemService.getItemById(item.getId(), 999L); + + assertNotNull(result); + verify(bookingRepository, never()).findLastBooking(anyLong(), any()); + } + + @Test + void getEntityById_shouldReturnItem() { + when(itemRepository.findById(anyLong())).thenReturn(Optional.of(item)); + + assertNotNull(itemService.getEntityById(item.getId())); + } + + @Test + void getEntityById_shouldThrowIfNotFound() { + when(itemRepository.findById(anyLong())).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> itemService.getEntityById(item.getId())); + } + + @Test + void getAllByOwner_shouldCallRepo() { + when(itemRepository.findAllByOwnerId(anyLong())).thenReturn(List.of(item)); + + List result = itemService.getAllByOwner(owner.getId()); + + assertEquals(1, result.size()); + } + + @Test + void search_shouldReturnEmptyListOnBlank() { + List result = itemService.search(" "); + + assertTrue(result.isEmpty()); + } + + @Test + void search_shouldReturnItems() { + when(itemRepository.search(anyString())).thenReturn(List.of(item)); + + List result = itemService.search("test"); + + assertEquals(1, result.size()); + } + + @Test + void addComment_shouldAddCommentIfApprovedBookingExists() { + User author = new User(); + author.setId(2L); + + Comment comment = new Comment(); + comment.setId(1L); + comment.setAuthor(author); + comment.setItem(item); + comment.setMessage("Nice!"); + comment.setCreated(LocalDateTime.now()); + + when(itemRepository.findById(anyLong())).thenReturn(Optional.of(item)); + when(userService.getEntityById(anyLong())).thenReturn(author); + when(bookingRepository.existsByItemIdAndBookerIdAndEndBeforeAndStatus( + anyLong(), anyLong(), any(), eq(BookingStatus.APPROVED)) + ).thenReturn(true); + when(commentRepository.save(any(Comment.class))).thenReturn(comment); + + CommentDto result = itemService.addComment(author.getId(), item.getId(), new ru.practicum.item.comment.NewCommentRequest("Nice!")); + + assertNotNull(result); + assertEquals("Nice!", result.getText()); + + verify(userService).getEntityById(author.getId()); + verify(commentRepository).save(any(Comment.class)); + } + + + @Test + void addComment_shouldThrowIfNoApprovedBooking() { + when(itemRepository.findById(anyLong())).thenReturn(Optional.of(item)); + when(userService.getEntityById(anyLong())).thenReturn(owner); + when(bookingRepository.existsByItemIdAndBookerIdAndEndBeforeAndStatus( + anyLong(), anyLong(), any(), eq(BookingStatus.APPROVED))) + .thenReturn(false); + + assertThrows(ValidationException.class, () -> + itemService.addComment(owner.getId(), item.getId(), new NewCommentRequest("test"))); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/ItemTest.java b/server/src/test/java/ru/practicum/shareit/item/ItemTest.java new file mode 100644 index 0000000..1dbb798 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/ItemTest.java @@ -0,0 +1,29 @@ +package ru.practicum.shareit.item; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ItemTest { + + @Test + void testEqualsSameId() { + Item item1 = Item.builder().id(1L).build(); + Item item2 = Item.builder().id(1L).build(); + Item item3 = Item.builder().id(2L).build(); + + assertEquals(item1, item2, "Objects with same id must be equal"); + assertNotEquals(item1, item3, "Objects with different id must not be equal"); + assertNotEquals(item1, null, "Equals must return false for null"); + assertNotEquals(item1, new Object(), "Equals must return false for other type"); + } + + @Test + void testHashCodeSameId() { + Item item1 = Item.builder().id(1L).build(); + Item item2 = Item.builder().id(1L).build(); + + assertEquals(item1.hashCode(), item2.hashCode(), + "Hash codes must be equal for same id"); + } +} \ No newline at end of file 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..a70a757 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/comment/CommentMapperTest.java @@ -0,0 +1,52 @@ +package ru.practicum.shareit.item.comment; + +import org.junit.jupiter.api.Test; +import ru.practicum.item.comment.CommentDto; +import ru.practicum.shareit.item.Item; +import ru.practicum.shareit.user.User; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +class CommentMapperTest { + + @Test + void toDto_shouldMapFieldsCorrectly() { + User author = User.builder().id(1L).name("John").build(); + Item item = Item.builder().id(2L).build(); + LocalDateTime created = LocalDateTime.now(); + + Comment comment = Comment.builder() + .id(3L) + .message("Good item!") + .author(author) + .item(item) + .created(created) + .build(); + + CommentDto dto = CommentMapper.toDto(comment); + + assertThat(dto.getId()).isEqualTo(3L); + assertThat(dto.getText()).isEqualTo("Good item!"); + assertThat(dto.getAuthorName()).isEqualTo("John"); + assertThat(dto.getCreated()).isEqualTo(created); + } + + @Test + void toComment_shouldMapFieldsCorrectly() { + CommentDto dto = CommentDto.builder() + .text("Nice one") + .build(); + + User author = User.builder().id(1L).name("Alice").build(); + Item item = Item.builder().id(2L).build(); + + Comment comment = CommentMapper.toComment(dto, item, author); + + assertThat(comment.getMessage()).isEqualTo("Nice one"); + assertThat(comment.getAuthor()).isEqualTo(author); + assertThat(comment.getItem()).isEqualTo(item); + assertThat(comment.getCreated()).isNotNull(); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/comment/CommentRepositoryTest.java b/server/src/test/java/ru/practicum/shareit/item/comment/CommentRepositoryTest.java new file mode 100644 index 0000000..b42bfc5 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/comment/CommentRepositoryTest.java @@ -0,0 +1,54 @@ +package ru.practicum.shareit.item.comment; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import ru.practicum.shareit.item.Item; +import ru.practicum.shareit.item.ItemRepository; +import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.UserRepository; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +class CommentRepositoryTest { + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private ItemRepository itemRepository; + + @Autowired + private UserRepository userRepository; + + @Test + void saveAndLoadComment() { + User author = userRepository.save(new User(null, "author@mail.com", "Author")); + User owner = userRepository.save(new User(null, "owner@mail.com", "Owner")); + + Item item = itemRepository.save(Item.builder() + .name("Drill") + .description("Tool") + .available(true) + .owner(owner) + .build()); + + Comment comment = Comment.builder() + .message("Nice tool!") + .item(item) + .author(author) + .created(LocalDateTime.now()) + .build(); + + Comment saved = commentRepository.save(comment); + + Comment loaded = commentRepository.findById(saved.getId()).orElseThrow(); + + assertThat(loaded.getMessage()).isEqualTo("Nice tool!"); + assertThat(loaded.getAuthor().getId()).isEqualTo(author.getId()); + assertThat(loaded.getItem().getId()).isEqualTo(item.getId()); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/item/comment/CommentTest.java b/server/src/test/java/ru/practicum/shareit/item/comment/CommentTest.java new file mode 100644 index 0000000..7851c11 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/item/comment/CommentTest.java @@ -0,0 +1,29 @@ +package ru.practicum.shareit.item.comment; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class CommentTest { + + @Test + void testEqualsSameId() { + Comment comment1 = Comment.builder().id(1L).build(); + Comment comment2 = Comment.builder().id(1L).build(); + Comment comment3 = Comment.builder().id(2L).build(); + + assertEquals(comment1, comment2, "Objects with same id must be equal"); + assertNotEquals(comment1, comment3, "Objects with different id must not be equal"); + assertNotEquals(comment1, null, "Equals must return false for null"); + assertNotEquals(comment1, new Object(), "Equals must return false for other type"); + } + + @Test + void testHashCodeAlwaysSame() { + Comment comment1 = Comment.builder().id(1L).build(); + Comment comment2 = Comment.builder().id(2L).build(); + + assertEquals(comment1.hashCode(), comment2.hashCode(), + "Hash codes should be equal because implementation always returns constant"); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/request/ItemRequestControllerTest.java b/server/src/test/java/ru/practicum/shareit/request/ItemRequestControllerTest.java new file mode 100644 index 0000000..b8d33dc --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/ItemRequestControllerTest.java @@ -0,0 +1,101 @@ +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.request.CreateItemRequestDto; +import ru.practicum.request.ItemRequestDto; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(ItemRequestController.class) +class ItemRequestControllerTest { + + @Autowired + private MockMvc mvc; + + @MockBean + private ItemRequestService itemRequestService; + + @Autowired + private ObjectMapper mapper; + + private ItemRequestDto requestDto; + private ItemRequestResponseDto responseDto; + + @BeforeEach + void setUp() { + requestDto = ItemRequestDto.builder() + .id(1L) + .description("Need a drill") + .created(LocalDateTime.now()) + .build(); + + responseDto = ru.practicum.shareit.request.ItemRequestResponseDto.builder() + .id(1L) + .description("Need a drill") + .created(LocalDateTime.now()) + .items(List.of()) + .build(); + } + + @Test + void createRequest_ShouldReturnCreatedRequest() throws Exception { + CreateItemRequestDto createDto = CreateItemRequestDto.builder() + .description("Need a drill") + .build(); + + when(itemRequestService.createRequest(1L, createDto)).thenReturn(requestDto); + + mvc.perform(post("/requests") + .header("X-Sharer-User-Id", 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(createDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(requestDto.getId())) + .andExpect(jsonPath("$.description").value(requestDto.getDescription())); + } + + @Test + void getOwnRequests_ShouldReturnList() throws Exception { + when(itemRequestService.getOwnRequests(1L)).thenReturn(List.of(responseDto)); + + mvc.perform(get("/requests") + .header("X-Sharer-User-Id", 1L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(responseDto.getId())) + .andExpect(jsonPath("$[0].description").value(responseDto.getDescription())); + } + + @Test + void getAllRequests_ShouldReturnList() throws Exception { + when(itemRequestService.getAllRequests(1L)).thenReturn(List.of(requestDto)); + + mvc.perform(get("/requests/all") + .header("X-Sharer-User-Id", 1L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(requestDto.getId())) + .andExpect(jsonPath("$[0].description").value(requestDto.getDescription())); + } + + @Test + void getRequest_ShouldReturnSingleRequest() throws Exception { + when(itemRequestService.getRequestById(1L, 1L)).thenReturn(responseDto); + + mvc.perform(get("/requests/1") + .header("X-Sharer-User-Id", 1L)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(responseDto.getId())) + .andExpect(jsonPath("$.description").value(responseDto.getDescription())); + } +} diff --git a/server/src/test/java/ru/practicum/shareit/request/ItemRequestDtoJsonTest.java b/server/src/test/java/ru/practicum/shareit/request/ItemRequestDtoJsonTest.java new file mode 100644 index 0000000..923e67a --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/ItemRequestDtoJsonTest.java @@ -0,0 +1,51 @@ +package ru.practicum.shareit.request; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import ru.practicum.request.ItemRequestDto; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +class ItemRequestDtoJsonTest { + + @Autowired + private JacksonTester json; + + @Test + void serializeRequestDto() throws Exception { + ItemRequestDto dto = ItemRequestDto.builder() + .id(5L) + .description("need drill") + .created(LocalDateTime.of(2025, 6, 16, 13, 0)) + .build(); + + var content = json.write(dto); + + assertThat(content).hasJsonPathNumberValue("$.id"); + assertThat(content).extractingJsonPathNumberValue("$.id").isEqualTo(5); + assertThat(content).extractingJsonPathStringValue("$.description").isEqualTo("need drill"); + assertThat(content).extractingJsonPathStringValue("$.created") + .isEqualTo("2025-06-16T13:00:00"); + } + + @Test + void roundTripDeserializeRequestDto() throws Exception { + ItemRequestDto original = ItemRequestDto.builder() + .id(8L) + .description("saw please") + .created(LocalDateTime.of(2025, 6, 16, 14, 30)) + .build(); + + String jsonString = json.write(original).getJson(); + ItemRequestDto parsed = json.parseObject(jsonString); + + assertThat(parsed.getId()).isEqualTo(original.getId()); + assertThat(parsed.getDescription()).isEqualTo(original.getDescription()); + assertThat(parsed.getCreated()).isEqualTo(original.getCreated()); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceImplTest.java b/server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceImplTest.java new file mode 100644 index 0000000..2bf90b7 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/request/ItemRequestServiceImplTest.java @@ -0,0 +1,131 @@ +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.request.CreateItemRequestDto; +import ru.practicum.shareit.item.Item; +import ru.practicum.shareit.item.ItemRepository; +import ru.practicum.shareit.user.User; +import ru.practicum.shareit.user.UserRepository; + +import java.time.LocalDateTime; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +class ItemRequestServiceImplTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ItemRequestRepository itemRequestRepository; + + @Autowired + private ItemRepository itemRepository; + + @Autowired + private ObjectMapper mapper; + + private User requester; + private User owner; + + @BeforeEach + void setUp() { + userRepository.deleteAll(); + + requester = userRepository.save(new User(null, "requester@mail.com", "Requester")); + owner = userRepository.save(new User(null, "11111@mail.com", "Owner")); + } + + @Test + void createRequest() throws Exception { + CreateItemRequestDto dto = new CreateItemRequestDto(); + dto.setDescription("Need a drill"); + + mvc.perform(post("/requests") + .header("X-Sharer-User-Id", requester.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(dto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.description", is("Need a drill"))); + } + + @Test + void getOwnRequests() throws Exception { + ItemRequest request = itemRequestRepository.save(ItemRequest.builder() + .description("Need a saw") + .requester(requester) + .created(LocalDateTime.now()) + .build()); + + itemRepository.save(Item.builder() + .name("Saw") + .description("Wood saw") + .available(true) + .owner(owner) + .requestId(request.getId()) + .build()); + + mvc.perform(get("/requests") + .header("X-Sharer-User-Id", requester.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].description", is("Need a saw"))) + .andExpect(jsonPath("$[0].items", hasSize(1))) + .andExpect(jsonPath("$[0].items[0].name", is("Saw"))); + } + + @Test + void getAllRequests() throws Exception { + itemRequestRepository.save(ItemRequest.builder() + .description("Need a hammer") + .requester(requester) + .created(LocalDateTime.now()) + .build()); + + mvc.perform(get("/requests/all") + .header("X-Sharer-User-Id", owner.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].description", is("Need a hammer"))); + } + + @Test + void getRequestById() throws Exception { + ItemRequest request = itemRequestRepository.save(ItemRequest.builder() + .description("Need a wrench") + .requester(requester) + .created(LocalDateTime.now()) + .build()); + + itemRepository.save(Item.builder() + .name("Wrench") + .description("Metal wrench") + .available(true) + .owner(owner) + .requestId(request.getId()) + .build()); + + mvc.perform(get("/requests/" + request.getId()) + .header("X-Sharer-User-Id", owner.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.description", is("Need a wrench"))) + .andExpect(jsonPath("$.items", hasSize(1))) + .andExpect(jsonPath("$.items[0].name", is("Wrench"))); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/UserControllerTest.java b/server/src/test/java/ru/practicum/shareit/user/UserControllerTest.java new file mode 100644 index 0000000..e94b595 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserControllerTest.java @@ -0,0 +1,83 @@ +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.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.practicum.user.UserDto; + +import java.util.List; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(UserController.class) +class UserControllerTest { + + @Autowired MockMvc mvc; + @Autowired ObjectMapper mapper; + @MockBean UserService userService; + + private UserDto userDto; + + + @BeforeEach + void setUp() { + userDto = UserDto.builder() + .id(1L) + .name("John Doe") + .email("john@example.com") + .build(); + } + + @Test + void createUser_ok() throws Exception { + User in = new User(null,"u@u.com","U"); + UserDto out = new UserDto(1L,"u@u.com","U"); + + when(userService.create(any())).thenReturn(out); + + mvc.perform(post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(in))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)); + } + + @Test + void getAllUsers_ok() throws Exception { + List list = List.of(new UserDto(1L,"a@a","A")); + + when(userService.getAll()).thenReturn(list); + + mvc.perform(get("/users")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].email").value("a@a")); + } + + @Test + void getById_ShouldReturnUser() throws Exception { + when(userService.getUserById(1L)).thenReturn(userDto); + + mvc.perform(get("/users/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(userDto.getId())) + .andExpect(jsonPath("$.name").value(userDto.getName())) + .andExpect(jsonPath("$.email").value(userDto.getEmail())); + + verify(userService).getUserById(1L); + } + + @Test + void delete_ShouldReturnNoContent() throws Exception { + mvc.perform(delete("/users/1")) + .andExpect(status().isNoContent()); + + verify(userService).delete(1L); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/UserDtoJsonTest.java b/server/src/test/java/ru/practicum/shareit/user/UserDtoJsonTest.java new file mode 100644 index 0000000..a9b484a --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserDtoJsonTest.java @@ -0,0 +1,46 @@ +package ru.practicum.shareit.user; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import ru.practicum.user.UserDto; + +import static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +class UserDtoJsonTest { + + @Autowired + private JacksonTester json; + + @Test + void serializeUserDto() throws Exception { + UserDto dto = UserDto.builder() + .id(7L) + .email("x@x.com") + .name("X") + .build(); + + JsonContent result = json.write(dto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(7); + assertThat(result).extractingJsonPathStringValue("$.email").isEqualTo("x@x.com"); + assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("X"); + } + + @Test + void deserializeUserDto() throws Exception { + UserDto original = UserDto.builder() + .id(1L) + .email("test@example.com") + .name("T") + .build(); + + String jsonStr = json.write(original).getJson(); + UserDto parsed = json.parseObject(jsonStr); + + assertThat(parsed).isEqualTo(original); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/UserMapperTest.java b/server/src/test/java/ru/practicum/shareit/user/UserMapperTest.java new file mode 100644 index 0000000..d270013 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserMapperTest.java @@ -0,0 +1,17 @@ +package ru.practicum.shareit.user; + +import org.junit.jupiter.api.Test; +import ru.practicum.user.UserDto; + +import static org.assertj.core.api.Assertions.*; + +class UserMapperTest { + @Test + void toDto() { + User u = new User(1L,"a@b.c","A"); + UserDto dto = UserMapper.toUserDto(u); + + assertThat(dto.getEmail()).isEqualTo("a@b.c"); + assertThat(dto.getName()).isEqualTo("A"); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/UserServiceImplTest.java b/server/src/test/java/ru/practicum/shareit/user/UserServiceImplTest.java new file mode 100644 index 0000000..e4f6fc5 --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserServiceImplTest.java @@ -0,0 +1,90 @@ +package ru.practicum.shareit.user; + +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.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.user.NewUserRequest; +import ru.practicum.user.UpdateUserRequest; +import ru.practicum.user.UserDto; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +@SpringBootTest +@AutoConfigureTestDatabase +@Transactional +class UserServiceImplTest { + + @Autowired + UserService userService; + @Autowired + UserRepository userRepository; + + @BeforeEach + void setUp() { + userRepository.deleteAll(); + } + + @Test + void createAndGetAllAndGetById() { + UserDto u1 = userService.create(NewUserRequest.builder() + .email("a@a.com") + .name("A") + .build()); + UserDto u2 = userService.create(NewUserRequest.builder() + .email("b@b.com") + .name("B") + .build()); + + List all = userService.getAll(); + assertThat(all).extracting(UserDto::getEmail).containsExactlyInAnyOrder("a@a.com","b@b.com"); + + UserDto found = userService.getUserById(u1.getId()); + assertThat(found.getEmail()).isEqualTo("a@a.com"); + } + + @Test + void updateAndDelete() { + UserDto u = userService.create(NewUserRequest.builder() + .email("x@x.com") + .name("X") + .build()); + Long id = u.getId(); + + UserDto updated = userService.update(UpdateUserRequest.builder() + .id(id) + .email("u@u.com") + .name("U") + .build()); + assertThat(updated.getEmail()).isEqualTo("u@u.com"); + userService.delete(id); + assertThatThrownBy(() -> userService.getUserById(id)) + .isInstanceOf(NotFoundException.class); + } + + @Test + void updateThrowsWhenUserNotFound() { + assertThatThrownBy(() -> userService.update( + UpdateUserRequest.builder().id(999L).email("x@x.com").build() + )).isInstanceOf(NotFoundException.class); + } + + @Test + void deleteThrowsWhenUserNotFound() { + assertThatThrownBy(() -> userService.delete(999L)) + .isInstanceOf(NotFoundException.class); + } + + @Test + void getEntityByIdReturnsCorrectUser() { + UserDto u = userService.create(NewUserRequest.builder() + .email("z@z.com").name("Z").build()); + User entity = userService.getEntityById(u.getId()); + assertThat(entity.getEmail()).isEqualTo("z@z.com"); + } +} \ No newline at end of file diff --git a/server/src/test/java/ru/practicum/shareit/user/UserTest.java b/server/src/test/java/ru/practicum/shareit/user/UserTest.java new file mode 100644 index 0000000..c36b06c --- /dev/null +++ b/server/src/test/java/ru/practicum/shareit/user/UserTest.java @@ -0,0 +1,41 @@ +package ru.practicum.shareit.user; + +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.*; + +class UserTest { + + @Test + void testEqualsAndHashCode() { + User u1 = new User(); + u1.setId(1L); + u1.setName("A"); + u1.setEmail("a@a.com"); + + User u2 = new User(); + u2.setId(1L); + u2.setName("A"); + u2.setEmail("a@a.com"); + + User u3 = new User(); + u3.setId(2L); + u3.setName("B"); + u3.setEmail("b@b.com"); + + assertThat(u1).isEqualTo(u2); + assertThat(u1).hasSameHashCodeAs(u2); + assertThat(u1).isNotEqualTo(u3); + } + + @Test + void testGettersAndSetters() { + User user = new User(); + user.setId(1L); + user.setName("Test"); + user.setEmail("test@test.com"); + + assertThat(user.getId()).isEqualTo(1L); + assertThat(user.getName()).isEqualTo("Test"); + assertThat(user.getEmail()).isEqualTo("test@test.com"); + } +} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/BookingRequestDto.java b/src/main/java/ru/practicum/shareit/booking/BookingRequestDto.java deleted file mode 100644 index 2cbb412..0000000 --- a/src/main/java/ru/practicum/shareit/booking/BookingRequestDto.java +++ /dev/null @@ -1,12 +0,0 @@ -package ru.practicum.shareit.booking; - -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -public class BookingRequestDto { - private Long itemId; - private LocalDateTime start; - private LocalDateTime end; -} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/booking/BookingService.java b/src/main/java/ru/practicum/shareit/booking/BookingService.java deleted file mode 100644 index 35ca661..0000000 --- a/src/main/java/ru/practicum/shareit/booking/BookingService.java +++ /dev/null @@ -1,15 +0,0 @@ -package ru.practicum.shareit.booking; - -import java.util.List; - -public interface BookingService { - Booking createBooking(Long userId, BookingRequestDto dto); - - Booking approveBooking(Long ownerId, Long bookingId, boolean approved); - - Booking getBookingById(Long userId, Long bookingId); - - List getBookingsByUser(Long userId, BookingState state); - - List getBookingsByOwner(Long ownerId, BookingState state); -} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/item/ItemService.java b/src/main/java/ru/practicum/shareit/item/ItemService.java deleted file mode 100644 index 897708a..0000000 --- a/src/main/java/ru/practicum/shareit/item/ItemService.java +++ /dev/null @@ -1,20 +0,0 @@ -package ru.practicum.shareit.item; - -import ru.practicum.shareit.item.comment.Comment; -import ru.practicum.shareit.item.comment.CommentDto; - -import java.util.List; - -public interface ItemService { - Item create(Long userId, ItemDto itemDto); - - Item update(Long userId, Long itemId, ItemDto itemDto); - - Item getItemById(Long itemId, Long requesterId); - - List getAllByOwner(Long userId); - - List search(String text); - - Comment addComment(Long userId, Long itemId, CommentDto dto); -} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/ItemRequest.java deleted file mode 100644 index fe7a595..0000000 --- a/src/main/java/ru/practicum/shareit/request/ItemRequest.java +++ /dev/null @@ -1,12 +0,0 @@ -package ru.practicum.shareit.request; - -import lombok.Data; -import java.time.LocalDateTime; - -@Data -public class ItemRequest { - private Long id; - private String description; - private Long requesterId; - private LocalDateTime created; -} \ No newline at end of file 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/ItemRequestDto.java b/src/main/java/ru/practicum/shareit/request/ItemRequestDto.java deleted file mode 100644 index c0ba079..0000000 --- a/src/main/java/ru/practicum/shareit/request/ItemRequestDto.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.request; - -/** - * TODO Sprint add-item-requests. - */ -public class ItemRequestDto { -} diff --git a/src/main/java/ru/practicum/shareit/user/UserMapper.java b/src/main/java/ru/practicum/shareit/user/UserMapper.java deleted file mode 100644 index 7cb4381..0000000 --- a/src/main/java/ru/practicum/shareit/user/UserMapper.java +++ /dev/null @@ -1,25 +0,0 @@ -package ru.practicum.shareit.user; - -public class UserMapper { - - public static UserDto toUserDto(User user) { - return UserDto.builder() - .id(user.getId()) - .name(user.getName()) - .email(user.getEmail()) - .build(); - } - - public static User toUser(UserDto userDto) { - return User.builder() - .name(userDto.getName()) - .email(userDto.getEmail()) - .build(); - } - - public static void updateUserFields(User user, UserDto userDto) { - if (userDto.getName() != null) { - user.setName(userDto.getName()); - } - } -} \ No newline at end of file diff --git a/src/main/java/ru/practicum/shareit/user/UserService.java b/src/main/java/ru/practicum/shareit/user/UserService.java deleted file mode 100644 index 4098213..0000000 --- a/src/main/java/ru/practicum/shareit/user/UserService.java +++ /dev/null @@ -1,15 +0,0 @@ -package ru.practicum.shareit.user; - -import java.util.List; - -public interface UserService { - User create(UserDto userDto); - - User update(UserDto userDto); - - User getUserById(Long id); - - List getAll(); - - void delete(Long id); -} \ No newline at end of file