Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6b6f77a
fix: changed name from violations to error because of tests
LightInTheFire Dec 9, 2025
434feb0
feat: add item requests
LightInTheFire Dec 9, 2025
cba4ce3
chore: added sorting to itemRequest find all call
LightInTheFire Dec 9, 2025
b40e2fc
feat: add modules separation
LightInTheFire Dec 9, 2025
fbf6060
feat: transfer user validation to gateway
LightInTheFire Dec 10, 2025
44de833
feat: add server url as env variable
LightInTheFire Dec 10, 2025
57f58fe
feat: remove all validation from server
LightInTheFire Dec 10, 2025
b8c3789
feat: add validation and controllers to gateway
LightInTheFire Dec 10, 2025
9b4f0a1
feat: add json tests for gateway
LightInTheFire Dec 10, 2025
1080480
feat: add controller tests
LightInTheFire Dec 10, 2025
cbeb9fa
feat: add exception handler tests
LightInTheFire Dec 10, 2025
083e398
fix: changed class name
LightInTheFire Dec 10, 2025
a4d718d
feat: add bookingcontroller test in gateway
LightInTheFire Dec 10, 2025
1029742
feat: more controller tests for gateway
LightInTheFire Dec 10, 2025
68e107d
feat: more gateway tests
LightInTheFire Dec 10, 2025
91aeb9e
feat: enum tests
LightInTheFire Dec 10, 2025
1435161
fix: remove unused constructor
LightInTheFire Dec 10, 2025
ca46593
fix
LightInTheFire Dec 10, 2025
307d9b8
feat: add service and mapper tests for server
LightInTheFire Dec 10, 2025
1a94b8e
feat: add more tests to booking and item services
LightInTheFire Dec 10, 2025
b5b6d57
feat: add dockerignore
LightInTheFire Dec 11, 2025
477640a
feat: add integration tests for services
LightInTheFire Dec 11, 2025
86ad366
feat: add lifecycle per class annotation
LightInTheFire Dec 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Ignore everything by default
*

# Include what we need
!**/src
!.mvn
!**/pom.xml
!mvnw
36 changes: 30 additions & 6 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,21 @@ services:
timeout: 5s
interval: 5s
retries: 10
shareit:
image: shareit:latest
build: .
container_name: shareit-app
server:
image: shareit-server:latest
build:
context: .
dockerfile: server/Dockerfile
container_name: server
restart: unless-stopped
ports:
- "8080:8080"
- "9090:9090"
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/shareit
SPRING_DATASOURCE_USERNAME: shareit_user
SPRING_DATASOURCE_PASSWORD: secret
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
test: wget --no-verbose --tries=1 --spider http://localhost:9090/actuator/health || exit 1
interval: 30s
timeout: 5s
start_period: 30s
Expand All @@ -37,5 +39,27 @@ services:
db:
condition: service_healthy

gateway:
image: shareit-gateway:latest
build:
context: .
dockerfile: gateway/Dockerfile
container_name: gateway
restart: unless-stopped
ports:
- "8080:8080"
environment:
SHAREIT_SERVER_URL: http://server:9090
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
interval: 30s
timeout: 5s
start_period: 30s
retries: 5
depends_on:
server:
condition: service_healthy


volumes:
postgres-data:
40 changes: 40 additions & 0 deletions gateway/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
FROM maven:3.9.11-amazoncorretto-21 AS builder
WORKDIR /application

COPY pom.xml ./

COPY gateway/pom.xml gateway/pom.xml
COPY server/pom.xml server/pom.xml

ENV MAVEN_OPTS="-Dmaven.repo.local=/app/.m2/repository"
RUN mvn -pl gateway -am dependency:go-offline -B
COPY gateway/src ./gateway/src

RUN mvn clean package -pl gateway -am -DskipTests

FROM amazoncorretto:21.0.8-alpine AS layers
WORKDIR /application
COPY --from=builder /application/gateway/target/*.jar app.jar
RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted

FROM amazoncorretto:21.0.8-alpine
VOLUME /tmp
RUN adduser -S spring-user
USER spring-user

WORKDIR /application

COPY --from=layers /application/extracted/dependencies/ ./
COPY --from=layers /application/extracted/spring-boot-loader/ ./
COPY --from=layers /application/extracted/snapshot-dependencies/ ./
COPY --from=layers /application/extracted/application/ ./

RUN java -XX:ArchiveClassesAtExit=app.jsa -Dspring.context.exit=onRefresh -jar app.jar & exit 0

ENV JAVA_CDS_OPTS="-XX:SharedArchiveFile=app.jsa -Xlog:class+load:file=/tmp/classload.log"
ENV JAVA_ERROR_FILE_OPTS="-XX:ErrorFile=/tmp/java_error.log"

ENTRYPOINT java \
$JAVA_ERROR_FILE_OPTS \
$JAVA_CDS_OPTS \
-jar app.jar
32 changes: 32 additions & 0 deletions gateway/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ru.practicum</groupId>
<artifactId>shareit</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>shareit-gateway</artifactId>

<name>ShareIt Gateway</name>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
12 changes: 12 additions & 0 deletions gateway/src/main/java/ru/practicum/shareit/ShareItGateway.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ru.practicum.shareit;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ShareItGateway {

public static void main(String[] args) {
SpringApplication.run(ShareItGateway.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package ru.practicum.shareit.booking;

import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestClient;
import ru.practicum.shareit.booking.dto.BookingDto;

@Validated
@RestController
@RequestMapping(path = "/bookings")
public class BookingController {
private static final String SHARER_USER_ID_HEADER = "X-Sharer-User-Id";
private final RestClient restClient;

@Autowired
public BookingController(@Value("${shareit-server.url}") String baseUrl) {
this.restClient = RestClient.builder()
.baseUrl(baseUrl.concat("/bookings"))
.build();
}

@PostMapping
public ResponseEntity<Object> createBooking(
@RequestHeader(SHARER_USER_ID_HEADER) long bookerId,
@RequestBody @Valid BookingDto bookingDto
) {
return restClient.post()
.contentType(MediaType.APPLICATION_JSON)
.body(bookingDto)
.header(SHARER_USER_ID_HEADER, String.valueOf(bookerId))
.retrieve()
.toEntity(Object.class);
}

@PatchMapping("{bookingId}")
public ResponseEntity<Object> approveBooking(
@PathVariable long bookingId,
@RequestParam boolean approved,
@RequestHeader(SHARER_USER_ID_HEADER) long ownerId
) {
return restClient.patch()
.uri(uriBuilder -> uriBuilder
.path("/" + bookingId)
.queryParam("approved", approved)
.build())
.contentType(MediaType.APPLICATION_JSON)
.header(SHARER_USER_ID_HEADER, String.valueOf(ownerId))
.retrieve()
.toEntity(Object.class);
}

@GetMapping("{bookingId}")
public ResponseEntity<Object> getBooking(
@PathVariable long bookingId,
@RequestHeader(SHARER_USER_ID_HEADER) long userId
) {
return restClient.get()
.uri("/" + bookingId)
.header(SHARER_USER_ID_HEADER, String.valueOf(userId))
.retrieve()
.toEntity(Object.class);
}

@GetMapping
public ResponseEntity<Object> getAllBookingsOfUser(
@RequestParam(defaultValue = "ALL") String state,
@RequestHeader(SHARER_USER_ID_HEADER) long userId
) {
BookingState bookingState = BookingState.fromString(state)
.orElseThrow(() -> new IllegalArgumentException("Invalid booking state"));

return restClient.get()
.uri(uriBuilder -> uriBuilder
.queryParam("state", bookingState)
.build())
.header(SHARER_USER_ID_HEADER, String.valueOf(userId))
.retrieve()
.toEntity(Object.class);
}

@GetMapping("/owner")
public ResponseEntity<Object> getBookingsByOwner(
@RequestParam(defaultValue = "ALL") String state,
@RequestHeader(SHARER_USER_ID_HEADER) long ownerId
) {
BookingState bookingState = BookingState.fromString(state)
.orElseThrow(() -> new IllegalArgumentException("Invalid booking state"));
return restClient.get()
.uri(uriBuilder -> uriBuilder
.path("/owner")
.queryParam("state", bookingState)
.build())
.header(SHARER_USER_ID_HEADER, String.valueOf(ownerId))
.retrieve()
.toEntity(Object.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package ru.practicum.shareit.exception.dto;

import java.util.List;

public record ValidationErrorResponse(List<Violation> error) { //violations are better name but test expect error
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package ru.practicum.shareit.exception.handler;

import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingRequestHeaderException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import ru.practicum.shareit.exception.dto.ErrorResponse;
import ru.practicum.shareit.exception.dto.ValidationErrorResponse;
import ru.practicum.shareit.exception.dto.Violation;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse onException(Exception ex) {
log.error("Error occurred while processing request {}", ex.getMessage());
return new ErrorResponse("internal server error",
"An error occurred while processing request");
}

@ExceptionHandler(HttpServerErrorException.class)
public ResponseEntity<Object> onHttpServerErrorException(HttpServerErrorException ex) {
log.error(ex.getMessage());
return ResponseEntity.status(ex.getStatusCode())
.contentType(MediaType.APPLICATION_JSON)
.body(ex.getResponseBodyAsByteArray());
}

@ExceptionHandler(HttpClientErrorException.class)
public ResponseEntity<Object> onHttpClientErrorException(HttpClientErrorException ex) {
log.error(ex.getMessage());
return ResponseEntity.status(ex.getStatusCode())
.contentType(MediaType.APPLICATION_JSON)
.body(ex.getResponseBodyAsByteArray());
}

@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ValidationErrorResponse onConstraintValidationException(
ConstraintViolationException ex
) {
final List<Violation> violations = ex.getConstraintViolations().stream()
.map(
violation -> new Violation(
violation.getPropertyPath().toString(),
violation.getMessage()
)
)
.collect(Collectors.toList());
log.warn(violations.toString());
return new ValidationErrorResponse(violations);
}

@ExceptionHandler(MissingRequestHeaderException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse onMissingRequestHeaderException(MissingRequestHeaderException ex) {
log.error(ex.getMessage());
return new ErrorResponse("missing header", ex.getMessage());
}

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ValidationErrorResponse onMethodArgumentNotValidException(
MethodArgumentNotValidException ex
) {
final List<Violation> violations = ex.getBindingResult().getFieldErrors().stream()
.map(error -> new Violation(error.getField(), error.getDefaultMessage()))
.collect(Collectors.toList());
log.warn(violations.toString());
return new ValidationErrorResponse(violations);
}

@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse onIllegalArgumentException(IllegalArgumentException ex) {
log.error(ex.getMessage());
return new ErrorResponse("illegal argument", ex.getMessage());
}
}
Loading
Loading