diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2721138 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +# Builder stage +FROM openjdk:17-jdk-alpine as builder +WORKDIR application +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} application.jar +RUN java -Djarmode=layertools -jar application.jar extract + +# Final stage +FROM openjdk:17-jdk-alpine +WORKDIR application +COPY --from=builder application/dependencies/ ./ +COPY --from=builder application/spring-boot-loader/ ./ +COPY --from=builder application/snapshot-dependencies/ ./ +COPY --from=builder application/application/ ./ +ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"] +EXPOSE 8080 diff --git a/car-sharing-app-db-model.mwb b/car-sharing-app-db-model.mwb deleted file mode 100644 index aee404e..0000000 Binary files a/car-sharing-app-db-model.mwb and /dev/null differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a748af9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +name: car-sharing-application +services: + mysqldb: + image: mysql:8.0.36-oracle + restart: unless-stopped + env_file: + - ./.env + environment: + - MYSQL_DATABASE=$MYSQLDB_DATABASE + - MYSQL_ROOT_PASSWORD=$MYSQLDB_ROOT_PASSWORD + ports: + - $MYSQLDB_LOCAL_PORT:$MYSQLDB_DOCKER_PORT + healthcheck: + test: [ "CMD-SHELL", "mysqladmin ping -h localhost -u${MYSQLDB_USER} -p${MYSQLDB_ROOT_PASSWORD}" ] + interval: 30s + timeout: 30s + retries: 30 + app: + depends_on: + - mysqldb + image: car-sharing-image + restart: on-failure + container_name: app + build: . + env_file: ./.env + ports: + - $SPRING_LOCAL_PORT:$SPRING_DOCKER_PORT + - $DEBUG_PORT:$DEBUG_PORT + environment: + SPRING_APPLICATION_JSON: '{ + "spring.datasource.url" : "jdbc:mysql://mysqldb:$MYSQLDB_DOCKER_PORT/$MYSQLDB_DATABASE?createDatabaseIfNotExist=true&characterEncoding=UTF-8&serverTimezone=UTC", + "spring.datasource.username" : "$MYSQLDB_USER", + "spring.datasource.password" : "$MYSQLDB_ROOT_PASSWORD", + "spring.jpa.hibernate.ddl-auto" : "validate", + "spring.jpa.show-sql" : "true", + "jwt.expiration" : "$JWT_EXPIRATION_TIME", + "jwt.secret" : "$JWT_SECRET", + "STRIPE_APY_KEY" : "$STRIPE_APY_KEY", + "STRIPE_SUCCESS_LINK" : "$STRIPE_SUCCESS_LINK", + "STRIPE_CANCEL_LINK" : "$STRIPE_CANCEL_LINK", + "TELEGRAM_BOT_TOKEN" : "$TELEGRAM_BOT_TOKEN", + "TELEGRAM_BOT_USERNAME" : "$TELEGRAM_BOT_USERNAME" + }' + JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" diff --git a/pom.xml b/pom.xml index 558d5fb..adc2b87 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,7 @@ 1.19.4 6.9.0 24.19.0 + 2.17.0 @@ -136,6 +137,17 @@ stripe-java ${stripe.version} + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + diff --git a/src/main/java/mate/academy/carsharing/annotation/UserRoleDescription.java b/src/main/java/mate/academy/carsharing/annotation/UserRoleDescription.java new file mode 100644 index 0000000..9daa30b --- /dev/null +++ b/src/main/java/mate/academy/carsharing/annotation/UserRoleDescription.java @@ -0,0 +1,12 @@ +package mate.academy.carsharing.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface UserRoleDescription { + +} diff --git a/src/main/java/mate/academy/carsharing/annotation/UserRoleDescriptionCustomizer.java b/src/main/java/mate/academy/carsharing/annotation/UserRoleDescriptionCustomizer.java new file mode 100644 index 0000000..a59aa17 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/annotation/UserRoleDescriptionCustomizer.java @@ -0,0 +1,31 @@ +package mate.academy.carsharing.annotation; + +import io.swagger.v3.oas.models.Operation; +import org.springdoc.core.customizers.OperationCustomizer; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; + +@Component +public class UserRoleDescriptionCustomizer implements OperationCustomizer { + @Override + public Operation customize(Operation operation, HandlerMethod handlerMethod) { + UserRoleDescription userRoleAnnotation = + handlerMethod.getMethodAnnotation(UserRoleDescription.class); + if (userRoleAnnotation != null) { + PreAuthorize preAuthorizeAnnotation = + handlerMethod.getMethodAnnotation(PreAuthorize.class); + if (preAuthorizeAnnotation != null + && preAuthorizeAnnotation.value().contains("ROLE_")) { + String description = operation.getDescription() == null + ? "" : (operation.getDescription()/* + "\n"*/); + int firstBraceIndex = preAuthorizeAnnotation.value().indexOf('('); + int lastBraceIndex = preAuthorizeAnnotation.value().lastIndexOf(')'); + String rolesString = preAuthorizeAnnotation.value() + .substring(firstBraceIndex + 1, lastBraceIndex); + operation.setDescription(description + " Required roles: " + rolesString + '.'); + } + } + return operation; + } +} diff --git a/src/main/java/mate/academy/carsharing/config/OpenApiConfig.java b/src/main/java/mate/academy/carsharing/config/OpenApiConfig.java new file mode 100644 index 0000000..d8a7fdf --- /dev/null +++ b/src/main/java/mate/academy/carsharing/config/OpenApiConfig.java @@ -0,0 +1,20 @@ +package mate.academy.carsharing.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.annotations.security.SecuritySchemes; +import org.springframework.context.annotation.Configuration; + +@Configuration +@OpenAPIDefinition(info = @Info(title = "Car-Sharing-App REST API", version = "1.0", + description = "REST API for Car-Sharing-App. Register, Login and use JWT token."), + security = {@SecurityRequirement(name = "bearerToken")} +) +@SecuritySchemes({@SecurityScheme(name = "bearerToken", type = SecuritySchemeType.HTTP, + scheme = "bearer", bearerFormat = "JWT")} +) +public class OpenApiConfig { +} diff --git a/src/main/java/mate/academy/carsharing/config/SecurityConfig.java b/src/main/java/mate/academy/carsharing/config/SecurityConfig.java index 8694f59..2b991f8 100644 --- a/src/main/java/mate/academy/carsharing/config/SecurityConfig.java +++ b/src/main/java/mate/academy/carsharing/config/SecurityConfig.java @@ -37,6 +37,9 @@ public SecurityFilterChain securityFilterChain(UserDetailsService userDetailsSer // list cars. .requestMatchers(HttpMethod.GET, "api/cars", "api/cars/{id}") .permitAll() + .requestMatchers(HttpMethod.GET, + "api/payments/cancel", "api/payments/success") + .permitAll() .requestMatchers("api/auth/**", "/swagger-ui/**", "/v3/api-docs/**") .permitAll() .anyRequest() diff --git a/src/main/java/mate/academy/carsharing/controller/AuthenticationController.java b/src/main/java/mate/academy/carsharing/controller/AuthenticationController.java index e32d891..d6e1e48 100644 --- a/src/main/java/mate/academy/carsharing/controller/AuthenticationController.java +++ b/src/main/java/mate/academy/carsharing/controller/AuthenticationController.java @@ -1,5 +1,6 @@ package mate.academy.carsharing.controller; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -17,7 +18,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -@Tag(name = "Authentication manager", description = "Endpoints for registration") +@Tag(name = "Authentication controller", description = "Endpoints for registration and login.") @RestController @RequiredArgsConstructor @RequestMapping("/api/auth") @@ -26,6 +27,7 @@ public class AuthenticationController { private final AuthenticationService authenticationService; @ResponseStatus(HttpStatus.CREATED) + @Operation(summary = "Register a new user.", description = "Register a new user.") @PostMapping("/registration") public UserResponseDto register(@RequestBody @Valid UserRegistrationRequestDto request) throws RegistrationException { @@ -33,6 +35,8 @@ public UserResponseDto register(@RequestBody @Valid UserRegistrationRequestDto r } @PostMapping("/login") + @Operation(summary = "Login to the app.", + description = "Login to the app, get JWT Token and use it to request data.") public UserLoginResponseDto login(@RequestBody @Valid UserLoginRequestDto requestDto) { return authenticationService.authentication(requestDto); } diff --git a/src/main/java/mate/academy/carsharing/controller/CarController.java b/src/main/java/mate/academy/carsharing/controller/CarController.java index 30f3647..ba7f5df 100644 --- a/src/main/java/mate/academy/carsharing/controller/CarController.java +++ b/src/main/java/mate/academy/carsharing/controller/CarController.java @@ -6,6 +6,7 @@ import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; +import mate.academy.carsharing.annotation.UserRoleDescription; import mate.academy.carsharing.dto.car.CarResponseDto; import mate.academy.carsharing.dto.car.CreateCarRequestDto; import mate.academy.carsharing.service.CarService; @@ -25,13 +26,14 @@ @RestController @RequiredArgsConstructor @RequestMapping(path = "/api/cars") -@Tag(name = "Car management", description = "Cars Management Endpoints") +@Tag(name = "Car management", description = "Cars Management Endpoints.") public class CarController { private final CarService carService; @ResponseStatus(HttpStatus.CREATED) @PreAuthorize("hasRole('ROLE_MANAGER')") - @Operation(summary = "Create a new car", description = "Create a new car") + @UserRoleDescription + @Operation(summary = "Create a new car.", description = "Create a new car.") @PostMapping public CarResponseDto create(@RequestBody @Valid CreateCarRequestDto carDto) { return carService.save(carDto); @@ -39,7 +41,8 @@ public CarResponseDto create(@RequestBody @Valid CreateCarRequestDto carDto) { @ResponseStatus(HttpStatus.NO_CONTENT) @PreAuthorize("hasRole('ROLE_MANAGER')") - @Operation(summary = "Delete a car by id", description = "Delete car with specified id") + @UserRoleDescription + @Operation(summary = "Delete a car by id.", description = "Delete car with specified id.") @DeleteMapping("/{id}") public void deleteById(@PathVariable Long id) { carService.deleteById(id); @@ -47,7 +50,8 @@ public void deleteById(@PathVariable Long id) { @ResponseStatus(HttpStatus.OK) @PreAuthorize("hasRole('ROLE_MANAGER')") - @Operation(summary = "Update a car by id", description = "Update car with specified id") + @UserRoleDescription + @Operation(summary = "Update a car by id.", description = "Update car with specified id.") @PutMapping("/{id}") public CarResponseDto updateById(@PathVariable Long id, @RequestBody @Valid CreateCarRequestDto requestDto) { @@ -56,7 +60,8 @@ public CarResponseDto updateById(@PathVariable Long id, @ResponseStatus(HttpStatus.OK) @PreAuthorize("hasAnyRole('ROLE_MANAGER', 'ROLE_CUSTOMER', 'ROLE_ANONYMOUS')") - @Operation(summary = "Get a car by id", description = "Get a car with specified id") + @UserRoleDescription + @Operation(summary = "Get a car by id.", description = "Get a car with specified id.") @GetMapping("/{id}") public CarResponseDto getById(@PathVariable Long id) { return carService.getById(id); @@ -64,10 +69,11 @@ public CarResponseDto getById(@PathVariable Long id) { @ResponseStatus(HttpStatus.OK) @PreAuthorize("permitAll()") - @Operation(summary = "Get all cars", description = "Get a list of all cars") + @UserRoleDescription + @Operation(summary = "Get all cars.", description = "Get a list of all cars.") @Parameter(name = "page", description = "page index, default value = 0") @Parameter(name = "size", description = "elements per page, default value = 20") - @Parameter(name = "sort", description = "sort criteria", example = "title,Desc") + @Parameter(name = "sort", description = "sort criteria", example = "brand,Desc") @GetMapping public List getAll(Pageable pageable) { return carService.getAll(pageable); diff --git a/src/main/java/mate/academy/carsharing/controller/PaymentController.java b/src/main/java/mate/academy/carsharing/controller/PaymentController.java index 4fdd08c..e0c2bfb 100644 --- a/src/main/java/mate/academy/carsharing/controller/PaymentController.java +++ b/src/main/java/mate/academy/carsharing/controller/PaymentController.java @@ -1,13 +1,16 @@ package mate.academy.carsharing.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; +import mate.academy.carsharing.annotation.UserRoleDescription; import mate.academy.carsharing.dto.payment.CreatePaymentRequestDto; import mate.academy.carsharing.dto.payment.PaymentResponseDto; import mate.academy.carsharing.dto.payment.PaymentSearchParametersDto; +import mate.academy.carsharing.dto.payment.RenewPaymentRequestDto; import mate.academy.carsharing.service.PaymentService; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; @@ -30,7 +33,8 @@ public class PaymentController { @ResponseStatus(HttpStatus.CREATED) @PreAuthorize("hasRole('ROLE_MANAGER')") - @Operation(summary = "Create new payment", description = "Create new payment session") + @UserRoleDescription + @Operation(summary = "Create new payment.", description = "Create new payment session.") @PostMapping() public PaymentResponseDto createPayment( @RequestBody @Valid CreatePaymentRequestDto requestDto) { @@ -39,6 +43,12 @@ public PaymentResponseDto createPayment( @ResponseStatus(HttpStatus.OK) @PreAuthorize("hasAnyRole('ROLE_MANAGER', 'ROLE_CUSTOMER')") + @UserRoleDescription + @Operation(summary = "Search payments.", description = "User can view own payments. " + + "Admin can view all payments.") + @Parameter(name = "page", description = "page index, default value = 0") + @Parameter(name = "size", description = "elements per page, default value = 20") + @Parameter(name = "sort", description = "sort criteria", example = "amountToPay,Desc") @GetMapping("/search") public List searchPayments(Authentication authentication, PaymentSearchParametersDto searchParameters, Pageable pageable) { @@ -47,8 +57,9 @@ public List searchPayments(Authentication authentication, @ResponseStatus(HttpStatus.OK) @PreAuthorize("permitAll()") - @Operation(summary = "Stripe redirection endpoint", - description = "Mark payment as success, send message to Telegram") + @UserRoleDescription + @Operation(summary = "Stripe redirection endpoint marks payment as success.", + description = "Mark payment as success, send message to Telegram.") @GetMapping("/success") public PaymentResponseDto processSuccessfulPayment( @RequestParam(name = "session_id") String sessionId) { @@ -57,11 +68,24 @@ public PaymentResponseDto processSuccessfulPayment( @ResponseStatus(HttpStatus.OK) @PreAuthorize("permitAll()") - @Operation(summary = "Stripe redirection endpoint", - description = "Mark payment as canceled, send message to Telegram") + @UserRoleDescription + @Operation(summary = "Stripe redirection endpoint marks payment as canceled", + description = "Marks payment as canceled, send message to Telegram.") @GetMapping("/cancel") public PaymentResponseDto processCanceledPayment( @RequestParam(name = "session_id") String sessionId) { return paymentService.processCanceledPayment(sessionId); } + + @ResponseStatus(HttpStatus.OK) + @PreAuthorize("hasAnyRole('ROLE_MANAGER', 'ROLE_CUSTOMER')") + @UserRoleDescription + @Operation(summary = "Renew expired session.", + description = "User can renew expired session and get new payment link.") + @PostMapping("/renew") + public PaymentResponseDto renewPaymentSession( + @RequestBody @Valid RenewPaymentRequestDto requestDto, + Authentication authentication) { + return paymentService.renewPaymentSession(requestDto.paymentId(), authentication.getName()); + } } diff --git a/src/main/java/mate/academy/carsharing/controller/RentalController.java b/src/main/java/mate/academy/carsharing/controller/RentalController.java index a43b454..009e521 100644 --- a/src/main/java/mate/academy/carsharing/controller/RentalController.java +++ b/src/main/java/mate/academy/carsharing/controller/RentalController.java @@ -6,6 +6,7 @@ import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; +import mate.academy.carsharing.annotation.UserRoleDescription; import mate.academy.carsharing.dto.rental.CreateRentalRequestDto; import mate.academy.carsharing.dto.rental.RentalResponseDto; import mate.academy.carsharing.dto.rental.RentalSearchParametersDto; @@ -22,7 +23,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -@Tag(name = "Rental managing", description = "Endpoint to rentals managing") +@Tag(name = "Rental managing", description = "Endpoint for managing rentals") @RequiredArgsConstructor @RestController @RequestMapping("api/rentals") @@ -31,7 +32,8 @@ public class RentalController { @ResponseStatus(HttpStatus.CREATED) @PreAuthorize("hasRole('ROLE_MANAGER')") - @Operation(summary = "Create a new rental", description = "Admin creates a new rental") + @UserRoleDescription + @Operation(summary = "Create a new rental.", description = "Admin can create a new rental.") @PostMapping() public RentalResponseDto create(@RequestBody @Valid CreateRentalRequestDto requestDto) { return rentalService.save(requestDto); @@ -39,8 +41,9 @@ public RentalResponseDto create(@RequestBody @Valid CreateRentalRequestDto reque @ResponseStatus(HttpStatus.OK) @PreAuthorize("hasRole('ROLE_MANAGER')") - @Operation(summary = "Get list of rentals", description = "Admin can get list of " - + "active/nonactive rentals for any number of specified users or for all users") + @UserRoleDescription + @Operation(summary = "Get list of rentals.", description = "Admin can get list of " + + "active/nonactive rentals for any number of specified users or for all users.") @Parameter(name = "user_id", description = "Get rentals for users with specified 'user_id'." + "To get rentals for all users do not specify any value.", example = "2, 57") @Parameter(name = "is_active", description = "Specify 'true' to get active rentals or " @@ -54,7 +57,8 @@ public List searchRentals( @ResponseStatus(HttpStatus.OK) @PreAuthorize("hasRole('ROLE_CUSTOMER')") - @Operation(summary = "Get rental info by id", description = "Get rental info by id") + @UserRoleDescription + @Operation(summary = "Get rental info by id.", description = "Get rental info by id.") @Parameter(name = "id", description = "Rental id", example = "247") @GetMapping("/{id}") public RentalResponseDto getRental(@PathVariable Long id, Authentication authentication) { @@ -63,7 +67,8 @@ public RentalResponseDto getRental(@PathVariable Long id, Authentication authent @ResponseStatus(HttpStatus.OK) @PreAuthorize("hasRole('ROLE_MANAGER')") - @Operation(summary = "Return rental", description = "Admin can return rental by id") + @UserRoleDescription + @Operation(summary = "Return rental.", description = "Admin can return rental by id.") @Parameter(name = "id", description = "Rental id", example = "247") @PostMapping("/{id}/return") public RentalResponseDto returnRental(@PathVariable Long id) { diff --git a/src/main/java/mate/academy/carsharing/controller/UserController.java b/src/main/java/mate/academy/carsharing/controller/UserController.java index 4ae06a9..487641c 100644 --- a/src/main/java/mate/academy/carsharing/controller/UserController.java +++ b/src/main/java/mate/academy/carsharing/controller/UserController.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import mate.academy.carsharing.annotation.UserRoleDescription; import mate.academy.carsharing.dto.user.UserResponseDto; import mate.academy.carsharing.dto.user.UserResponseDtoWithRoles; import mate.academy.carsharing.dto.user.UserUpdateInfoRequestDto; @@ -21,7 +22,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -@Tag(name = "Users managing", description = "Endpoints to user managing") +@Tag(name = "Users managing", description = "Endpoints for managing users") @RestController @RequiredArgsConstructor @RequestMapping("/api/users") @@ -29,8 +30,10 @@ public class UserController { private final UserService userService; @ResponseStatus(HttpStatus.OK) - @Operation(summary = "Update user role", description = "Manager updates role for specify user") + @Operation(summary = "Update user role.", + description = "Manager updates role for specify user.") @PreAuthorize("hasRole('ROLE_MANAGER')") + @UserRoleDescription @PutMapping("/{id}/role") public UserResponseDtoWithRoles updateRole(@PathVariable Long id, @RequestBody @Valid UserUpdateRoleRequestDto requestDto) { @@ -39,8 +42,9 @@ public UserResponseDtoWithRoles updateRole(@PathVariable Long id, @ResponseStatus(HttpStatus.OK) @PreAuthorize("hasRole('ROLE_CUSTOMER')") - @Operation(summary = "Update info about user", - description = "Update firstName, lastName or password") + @UserRoleDescription + @Operation(summary = "Update info about user.", + description = "User can update firstName, lastName or password.") @PatchMapping("/me") public UserResponseDto updateUserInfo(Authentication authentication, @RequestBody @Valid UserUpdateInfoRequestDto requestDto) { @@ -49,7 +53,8 @@ public UserResponseDto updateUserInfo(Authentication authentication, @ResponseStatus(HttpStatus.OK) @PreAuthorize("hasAnyRole('ROLE_MANAGER', 'ROLE_CUSTOMER')") - @Operation(summary = "Get user info", description = "Get info about user") + @UserRoleDescription + @Operation(summary = "Get user info.", description = "Get info about user.") @GetMapping("/me") public UserResponseDtoWithRoles getUserInfo(Authentication authentication) { return userService.getUserInfo(authentication.getName()); diff --git a/src/main/java/mate/academy/carsharing/dto/payment/RenewPaymentRequestDto.java b/src/main/java/mate/academy/carsharing/dto/payment/RenewPaymentRequestDto.java new file mode 100644 index 0000000..53906a1 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/dto/payment/RenewPaymentRequestDto.java @@ -0,0 +1,8 @@ +package mate.academy.carsharing.dto.payment; + +import jakarta.validation.constraints.NotNull; + +public record RenewPaymentRequestDto( + @NotNull + Long paymentId) { +} diff --git a/src/main/java/mate/academy/carsharing/dto/user/UserUpdateInfoRequestDto.java b/src/main/java/mate/academy/carsharing/dto/user/UserUpdateInfoRequestDto.java index 9aeb369..91bf687 100644 --- a/src/main/java/mate/academy/carsharing/dto/user/UserUpdateInfoRequestDto.java +++ b/src/main/java/mate/academy/carsharing/dto/user/UserUpdateInfoRequestDto.java @@ -1,10 +1,12 @@ package mate.academy.carsharing.dto.user; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; public record UserUpdateInfoRequestDto( - @NotNull(message = "First name cannot be null") String firstName, - @NotNull(message = "Last name cannot be null") - String lastName) { + String lastName, + @Size(min = 8, message = PASSWORD_MUST_CONTAIN_AT_LEAST_8_CHARACTERS) + String password) { + private static final String PASSWORD_MUST_CONTAIN_AT_LEAST_8_CHARACTERS + = "Password must contain at least 8 characters"; } diff --git a/src/main/java/mate/academy/carsharing/exception/CustomGlobalExceptionHandler.java b/src/main/java/mate/academy/carsharing/exception/CustomGlobalExceptionHandler.java index 3c817e7..b987682 100644 --- a/src/main/java/mate/academy/carsharing/exception/CustomGlobalExceptionHandler.java +++ b/src/main/java/mate/academy/carsharing/exception/CustomGlobalExceptionHandler.java @@ -84,4 +84,14 @@ protected ResponseEntity handlePropertyReferenceException( .status(HttpStatus.BAD_REQUEST) .body(body); } + + @ExceptionHandler(PaymentException.class) + protected ResponseEntity handlePaymentException(PaymentException e) { + Map body = new LinkedHashMap<>(); + body.put("message", e.getMessage()); + body.put("timestamp", LocalDateTime.now()); + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(body); + } } diff --git a/src/main/java/mate/academy/carsharing/exception/PaymentException.java b/src/main/java/mate/academy/carsharing/exception/PaymentException.java new file mode 100644 index 0000000..498f4be --- /dev/null +++ b/src/main/java/mate/academy/carsharing/exception/PaymentException.java @@ -0,0 +1,7 @@ +package mate.academy.carsharing.exception; + +public class PaymentException extends RuntimeException { + public PaymentException(String message) { + super(message); + } +} diff --git a/src/main/java/mate/academy/carsharing/mapper/PaymentMapper.java b/src/main/java/mate/academy/carsharing/mapper/PaymentMapper.java index 606f8de..4e955be 100644 --- a/src/main/java/mate/academy/carsharing/mapper/PaymentMapper.java +++ b/src/main/java/mate/academy/carsharing/mapper/PaymentMapper.java @@ -11,7 +11,7 @@ public interface PaymentMapper { @Mapping(target = "rentalId", source = "rental.id") - @Mapping(target = "userId", source = "user.id") + @Mapping(target = "userId", source = "rental.user.id") PaymentResponseDto toDto(Payment payment); Payment toModel(CreatePaymentRequestDto requestDto); diff --git a/src/main/java/mate/academy/carsharing/model/Car.java b/src/main/java/mate/academy/carsharing/model/Car.java index eedf35c..28f6fe9 100644 --- a/src/main/java/mate/academy/carsharing/model/Car.java +++ b/src/main/java/mate/academy/carsharing/model/Car.java @@ -35,7 +35,7 @@ public class Car { private Integer inventory; @Column(name = "daily_fee", nullable = false) private BigDecimal dailyFee; - @Column(name = "deleted", nullable = false) + @Column(name = "deleted", nullable = false, columnDefinition = "TINYINT") private boolean deleted = false; public enum Type { diff --git a/src/main/java/mate/academy/carsharing/model/Payment.java b/src/main/java/mate/academy/carsharing/model/Payment.java index 08a27a7..521fb5d 100644 --- a/src/main/java/mate/academy/carsharing/model/Payment.java +++ b/src/main/java/mate/academy/carsharing/model/Payment.java @@ -34,11 +34,6 @@ public class Payment { @ManyToOne @JoinColumn(name = "rental_id", nullable = false) private Rental rental; - @ToString.Exclude - @EqualsAndHashCode.Exclude - @ManyToOne - @JoinColumn(name = "user_id", nullable = false) - private User user; @Column(nullable = false) @Enumerated(EnumType.STRING) private Status status; @@ -51,7 +46,7 @@ public class Payment { private String sessionId; @Column(name = "amount_to_pay", nullable = false) private BigDecimal amountToPay; - @Column(name = "deleted", nullable = false) + @Column(name = "deleted", nullable = false, columnDefinition = "TINYINT") private boolean deleted = false; public enum Status { diff --git a/src/main/java/mate/academy/carsharing/model/Rental.java b/src/main/java/mate/academy/carsharing/model/Rental.java index cc5aa81..a1e2c31 100644 --- a/src/main/java/mate/academy/carsharing/model/Rental.java +++ b/src/main/java/mate/academy/carsharing/model/Rental.java @@ -42,6 +42,6 @@ public class Rental { @ManyToOne @JoinColumn(name = "user_id", nullable = false) private User user; - @Column(name = "deleted", nullable = false) + @Column(name = "deleted", nullable = false, columnDefinition = "TINYINT") private boolean deleted; } diff --git a/src/main/java/mate/academy/carsharing/model/TelegramUserInfo.java b/src/main/java/mate/academy/carsharing/model/TelegramUserInfo.java index e01298b..f1cee43 100644 --- a/src/main/java/mate/academy/carsharing/model/TelegramUserInfo.java +++ b/src/main/java/mate/academy/carsharing/model/TelegramUserInfo.java @@ -28,6 +28,6 @@ public class TelegramUserInfo { @OneToOne @JoinColumn(name = "user_id", referencedColumnName = "id") private User user; - @Column(name = "deleted", nullable = false) + @Column(name = "deleted", nullable = false, columnDefinition = "TINYINT") private boolean deleted = false; } diff --git a/src/main/java/mate/academy/carsharing/model/User.java b/src/main/java/mate/academy/carsharing/model/User.java index 250982f..2ed90a9 100644 --- a/src/main/java/mate/academy/carsharing/model/User.java +++ b/src/main/java/mate/academy/carsharing/model/User.java @@ -47,7 +47,7 @@ public class User implements UserDetails { inverseJoinColumns = @JoinColumn(name = "role_id") ) private Set roles = new HashSet<>(); - @Column(name = "deleted", nullable = false) + @Column(name = "deleted", nullable = false, columnDefinition = "TINYINT") private boolean deleted = false; @Override diff --git a/src/main/java/mate/academy/carsharing/repository/payment/PaymentRepository.java b/src/main/java/mate/academy/carsharing/repository/payment/PaymentRepository.java index ebeee3f..c4948c0 100644 --- a/src/main/java/mate/academy/carsharing/repository/payment/PaymentRepository.java +++ b/src/main/java/mate/academy/carsharing/repository/payment/PaymentRepository.java @@ -1,6 +1,7 @@ package mate.academy.carsharing.repository.payment; import java.math.BigDecimal; +import java.util.List; import java.util.Optional; import mate.academy.carsharing.model.Payment; import mate.academy.carsharing.model.Rental; @@ -19,4 +20,9 @@ public interface PaymentRepository extends JpaRepository { BigDecimal getSumByRentalAndPaymentStatus(Rental rental, Payment.Status paymentStatus); Optional findBySessionId(String sessionId); + + List findAllByStatus(Payment.Status status); + + @Query("SELECT p FROM Payment p WHERE p.status = :paymentStatus AND p.rental.user.id = :userId") + List getAllByUserIdAndPaymentStatus(Long userId, Payment.Status paymentStatus); } diff --git a/src/main/java/mate/academy/carsharing/service/PaymentService.java b/src/main/java/mate/academy/carsharing/service/PaymentService.java index 4fc13f0..b31ec45 100644 --- a/src/main/java/mate/academy/carsharing/service/PaymentService.java +++ b/src/main/java/mate/academy/carsharing/service/PaymentService.java @@ -16,4 +16,6 @@ List search(String email, PaymentSearchParametersDto searchP PaymentResponseDto processSuccessfulPayment(String sessionId); PaymentResponseDto processCanceledPayment(String sessionId); + + PaymentResponseDto renewPaymentSession(Long paymentId, String email); } diff --git a/src/main/java/mate/academy/carsharing/service/impl/RentalServiceImpl.java b/src/main/java/mate/academy/carsharing/service/impl/RentalServiceImpl.java index ab77cce..a5e9adb 100644 --- a/src/main/java/mate/academy/carsharing/service/impl/RentalServiceImpl.java +++ b/src/main/java/mate/academy/carsharing/service/impl/RentalServiceImpl.java @@ -13,9 +13,11 @@ import mate.academy.carsharing.exception.RentalException; import mate.academy.carsharing.mapper.RentalMapper; import mate.academy.carsharing.model.Car; +import mate.academy.carsharing.model.Payment; import mate.academy.carsharing.model.Rental; import mate.academy.carsharing.model.User; import mate.academy.carsharing.repository.car.CarRepository; +import mate.academy.carsharing.repository.payment.PaymentRepository; import mate.academy.carsharing.repository.rental.RentalRepository; import mate.academy.carsharing.repository.rental.RentalSpecificationBuilder; import mate.academy.carsharing.repository.user.UserRepository; @@ -36,6 +38,7 @@ public class RentalServiceImpl implements RentalService { private final RentalRepository rentalRepository; private final UserRepository userRepository; private final CarRepository carRepository; + private final PaymentRepository paymentRepository; private final RentalMapper rentalMapper; private final RentalSpecificationBuilder rentalSpecificationBuilder; private final NotificationService notificationService; @@ -44,6 +47,9 @@ public class RentalServiceImpl implements RentalService { @Override @Transactional public RentalResponseDto save(CreateRentalRequestDto requestDto) { + if (!isAllowedToRentCar(requestDto.userId())) { + throw new RentalException("User can't rent a car. There are expired payments."); + } Car car = getCarById(requestDto.carId()); checkIsCarAvailable(requestDto, car); car.setInventory(car.getInventory() - 1); @@ -161,4 +167,10 @@ private Car getCarById(Long id) { return carRepository.findById(id) .orElseThrow(() -> new EntityNotFoundException("Can't find car by id: " + id)); } + + private boolean isAllowedToRentCar(Long userId) { + List expiredPayments = paymentRepository.getAllByUserIdAndPaymentStatus(userId, + Payment.Status.EXPIRED); + return expiredPayments.isEmpty(); + } } diff --git a/src/main/java/mate/academy/carsharing/service/impl/StripePaymentServiceImpl.java b/src/main/java/mate/academy/carsharing/service/impl/StripePaymentServiceImpl.java index 1b15bf8..bb78d22 100644 --- a/src/main/java/mate/academy/carsharing/service/impl/StripePaymentServiceImpl.java +++ b/src/main/java/mate/academy/carsharing/service/impl/StripePaymentServiceImpl.java @@ -2,7 +2,6 @@ import com.stripe.exception.StripeException; import com.stripe.model.checkout.Session; -import jakarta.persistence.EntityNotFoundException; import java.math.BigDecimal; import java.math.RoundingMode; import java.net.MalformedURLException; @@ -15,6 +14,8 @@ import mate.academy.carsharing.dto.payment.CreatePaymentRequestDto; import mate.academy.carsharing.dto.payment.PaymentResponseDto; import mate.academy.carsharing.dto.payment.PaymentSearchParametersDto; +import mate.academy.carsharing.exception.EntityNotFoundException; +import mate.academy.carsharing.exception.PaymentException; import mate.academy.carsharing.mapper.PaymentMapper; import mate.academy.carsharing.model.Payment; import mate.academy.carsharing.model.Rental; @@ -30,6 +31,7 @@ import mate.academy.carsharing.stripe.StripeSessionProvider; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @RequiredArgsConstructor @@ -100,7 +102,7 @@ public PaymentResponseDto processSuccessfulPayment(String sessionId) { String message = String.format("Payment with id: %d for the amount: %s successful!", payment.getId(), payment.getAmountToPay().divide(CONVERT_TO_CENT, RoundingMode.HALF_UP)); - notificationService.sendNotification(payment.getUser().getId(), message); + notificationService.sendNotification(payment.getRental().getUser().getId(), message); } return paymentMapper.toDto(paymentRepository.save(payment)); } @@ -109,21 +111,68 @@ public PaymentResponseDto processSuccessfulPayment(String sessionId) { public PaymentResponseDto processCanceledPayment(String sessionId) { Payment payment = getPaymentBySessionId(sessionId); payment.setStatus(Payment.Status.CANCEL); - notificationService.sendNotification(payment.getUser().getId(), + notificationService.sendNotification(payment.getRental().getUser().getId(), "Payment failure! The payment can be made later within 24 hours!"); return paymentMapper.toDto(paymentRepository.save(payment)); } + @Scheduled(cron = "0 * * * * *") + public void checkExpiredStripeSessions() { + List payments = paymentRepository.findAllByStatus(Payment.Status.PENDING); + for (Payment payment : payments) { + try { + Session session = stripeSessionProvider.retrieveSession(payment.getSessionId()); + if ("expired".equals(session.getStatus())) { + payment.setStatus(Payment.Status.EXPIRED); + paymentRepository.save(payment); + } + } catch (StripeException e) { + throw new RuntimeException("Can't retrieve session for payment:" + payment, e); + } + } + } + + @Override + public PaymentResponseDto renewPaymentSession(Long paymentId, String email) { + Payment payment = paymentRepository.findById(paymentId).orElseThrow( + () -> new EntityNotFoundException("Can't find payment with id: " + paymentId)); + Rental rental = payment.getRental(); + User userByAuthentication = getUserByEmail(email); + User userByRental = rental.getUser(); + Role roleManager = getRoleByName(Role.RoleName.ROLE_MANAGER); + if (!userByAuthentication.getRoles().contains(roleManager)) { + if (!userByRental.getEmail().equals(email)) { + throw new PaymentException("You do not have permission to renew this session"); + } + } + if (payment.getStatus().equals(Payment.Status.PAID)) { + throw new PaymentException("This payment session cannot be renewed"); + } + if (payment.getStatus().equals(Payment.Status.PENDING)) { + throw new PaymentException("No need to renew. The session is active"); + } + try { + Session newSession = stripeSessionProvider + .createStripeSession(calculateTotalSum(rental), "Rental repayment"); + payment.setStatus(Payment.Status.PENDING); + payment.setSessionId(newSession.getId()); + payment.setSessionUrl(new URL(newSession.getUrl())); + paymentRepository.save(payment); + return paymentMapper.toDto(payment); + } catch (StripeException | MalformedURLException e) { + throw new RuntimeException(e); + } + } + private Payment getPaymentBySessionId(String sessionId) { - return paymentRepository.findBySessionId(sessionId) - .orElseThrow(() -> new EntityNotFoundException( - "Can't find payment with session id: " + sessionId)); + return paymentRepository.findBySessionId(sessionId).orElseThrow( + () -> new EntityNotFoundException("Can't find payment with session id: " + + sessionId)); } private User getUserByEmail(String email) { return userRepository.findByEmail(email).orElseThrow( - () -> new EntityNotFoundException("Can't find user with email: " + email) - ); + () -> new EntityNotFoundException("Can't find user with email: " + email)); } private Role getRoleByName(Role.RoleName roleName) { @@ -133,10 +182,8 @@ private Role getRoleByName(Role.RoleName roleName) { } private Rental getRentalById(Long rentalId) { - return rentalRepository.findById(rentalId) - .orElseThrow( - () -> new EntityNotFoundException("Can't find rental with id: " + rentalId) - ); + return rentalRepository.findById(rentalId).orElseThrow( + () -> new EntityNotFoundException("Can't find rental with id: " + rentalId)); } private Payment createPayment(Rental rental, BigDecimal totalSum) { @@ -144,7 +191,6 @@ private Payment createPayment(Rental rental, BigDecimal totalSum) { payment.setAmountToPay(totalSum); payment.setStatus(Payment.Status.PENDING); payment.setRental(rental); - payment.setUser(rental.getUser()); if (rental.getReturnDate().isAfter(LocalDate.now())) { payment.setType(Payment.Type.PAYMENT); return payment; diff --git a/src/main/java/mate/academy/carsharing/service/impl/UserServiceImpl.java b/src/main/java/mate/academy/carsharing/service/impl/UserServiceImpl.java index eff0006..5c590cb 100644 --- a/src/main/java/mate/academy/carsharing/service/impl/UserServiceImpl.java +++ b/src/main/java/mate/academy/carsharing/service/impl/UserServiceImpl.java @@ -54,8 +54,18 @@ public UserResponseDtoWithRoles updateUserRole(Long id, @Override public UserResponseDto updateUserInfo(String email, UserUpdateInfoRequestDto requestDto) { User user = getUserByEmail(email); - user.setFirstName(requestDto.firstName()); - user.setLastName(requestDto.lastName()); + String newFirstName = requestDto.firstName(); + if (newFirstName != null && !newFirstName.isBlank()) { + user.setFirstName(newFirstName); + } + String newLastName = requestDto.lastName(); + if (newLastName != null && !newLastName.isBlank()) { + user.setLastName(newLastName); + } + String newPassword = requestDto.password(); + if (newPassword != null && !newPassword.isBlank()) { + user.setPassword(passwordEncoder.encode(newPassword)); + } return userMapper.toDto(userRepository.save(user)); } diff --git a/src/main/resources/db/changelog/changes/08-create-payments-table.yaml b/src/main/resources/db/changelog/changes/08-create-payments-table.yaml index 6df376e..d6d930c 100644 --- a/src/main/resources/db/changelog/changes/08-create-payments-table.yaml +++ b/src/main/resources/db/changelog/changes/08-create-payments-table.yaml @@ -20,13 +20,13 @@ databaseChangeLog: nullable: false foreignKeyName: fk_payments_rentals references: rentals(id) - - column: - name: user_id - type: bigint - constraints: - nullable: false - foreignKeyName: fk_payments_users - references: users(id) +# - column: +# name: user_id +# type: bigint +# constraints: +# nullable: false +# foreignKeyName: fk_payments_users +# references: users(id) - column: name: status type: "ENUM('PAID', 'PENDING', 'CANCEL', 'EXPIRED')" diff --git a/src/main/resources/db/changelog/changes/09-add-manager-and-2_customers-into-users-table.yaml b/src/main/resources/db/changelog/changes/09-add-manager-and-2_customers-into-users-table.yaml new file mode 100644 index 0000000..cbdb588 --- /dev/null +++ b/src/main/resources/db/changelog/changes/09-add-manager-and-2_customers-into-users-table.yaml @@ -0,0 +1,87 @@ +databaseChangeLog: + - changeSet: + id: add-manager-and-2_customers-into-users-table + comment: Add users and set roles for them. 'ROLE_MANAGER' id=1, 'ROLE_CUSTOMER' id=2 + author: Krasnov-Maksim + changes: + - insert: + tableName: users + columns: + - column: + name: email + value: "manager@mail.com" + - column: + name: password + value: "$2a$10$OshhtpSVoWVzyAxs2ZNBK.raK8CWFdSM5Q.9emAu71nt8qnMXmrTq" + - column: + name: first_name + value: "Manager" + - column: + name: last_name + value: "Cool" + - column: + name: deleted + valueBoolean: false + - insert: + tableName: users_roles + columns: + - column: + name: user_id + valueNumeric: 1 + - column: + name: role_id + valueNumeric: 1 + - insert: + tableName: users + columns: + - column: + name: email + value: "john@mail.com" + - column: + name: password + value: "$2a$10$r2vN5pGfhiiz2kQJGSDUFe/OGhh99uyNb4bUntg5Di3D1sOtKgjni" + - column: + name: first_name + value: "John" + - column: + name: last_name + value: "Doe" + - column: + name: deleted + valueBoolean: false + - insert: + tableName: users_roles + columns: + - column: + name: user_id + valueNumeric: 2 + - column: + name: role_id + valueNumeric: 2 + - insert: + tableName: users + columns: + - column: + name: email + value: "tom@mail.com" + - column: + name: password + value: "$2a$10$V4nKPAHV.H41QEazjPYad.aOYw1bOdlUBmtvG8SEhygS//0YMOGGq" + - column: + name: first_name + value: "Tom" + - column: + name: last_name + value: "Foo" + - column: + name: deleted + valueBoolean: false + - insert: + tableName: users_roles + columns: + - column: + name: user_id + valueNumeric: 3 + - column: + name: role_id + valueNumeric: 2 diff --git a/src/main/resources/db/changelog/changes/10-add-2_cars-into-cars-table.yaml b/src/main/resources/db/changelog/changes/10-add-2_cars-into-cars-table.yaml new file mode 100644 index 0000000..7e3ad10 --- /dev/null +++ b/src/main/resources/db/changelog/changes/10-add-2_cars-into-cars-table.yaml @@ -0,0 +1,47 @@ +databaseChangeLog: + - changeSet: + id: add-2_cars-into-cars-table + author: Krasnov-Maksim + changes: + - insert: + tableName: cars + columns: + - column: + name: model + value: "Q5" + - column: + name: brand + value: "Audi" + - column: + name: inventory + valueNumeric: 10 + - column: + name: daily_fee + valueNumeric: 50.50 + - column: + name: type + value: "SUV" + - column: + name: deleted + valueBoolean: false + - insert: + tableName: cars + columns: + - column: + name: model + value: "X5" + - column: + name: brand + value: "BMW" + - column: + name: inventory + valueNumeric: 10 + - column: + name: daily_fee + valueNumeric: 60.50 + - column: + name: type + value: "SUV" + - column: + name: deleted + valueBoolean: false diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index cb53944..459fc7b 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -15,3 +15,7 @@ databaseChangeLog: file: db/changelog/changes/07-create-telegram_user_info-table.yaml - include: file: db/changelog/changes/08-create-payments-table.yaml + - include: + file: db/changelog/changes/09-add-manager-and-2_customers-into-users-table.yaml + - include: + file: db/changelog/changes/10-add-2_cars-into-cars-table.yaml diff --git a/src/test/java/mate/academy/carsharing/CarSharingAppApplicationTests.java b/src/test/java/mate/academy/carsharing/CarSharingAppApplicationTests.java index 5aea202..55d9767 100644 --- a/src/test/java/mate/academy/carsharing/CarSharingAppApplicationTests.java +++ b/src/test/java/mate/academy/carsharing/CarSharingAppApplicationTests.java @@ -1,8 +1,9 @@ package mate.academy.carsharing; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; -//@SpringBootTest +@SpringBootTest class CarSharingAppApplicationTests { @Test diff --git a/src/test/java/mate/academy/carsharing/config/CustomMySqlContainer.java b/src/test/java/mate/academy/carsharing/config/CustomMySqlContainer.java new file mode 100644 index 0000000..ef1e7b6 --- /dev/null +++ b/src/test/java/mate/academy/carsharing/config/CustomMySqlContainer.java @@ -0,0 +1,33 @@ +package mate.academy.carsharing.config; + +import org.testcontainers.containers.MySQLContainer; + +public class CustomMySqlContainer extends MySQLContainer { + private static final String DB_IMAGE = "mysql"; + + private static CustomMySqlContainer mySqlContainer; + + public CustomMySqlContainer() { + super(DB_IMAGE); + } + + public static synchronized CustomMySqlContainer getInstance() { + if (mySqlContainer == null) { + mySqlContainer = new CustomMySqlContainer(); + } + return mySqlContainer; + } + + @Override + public void start() { + super.start(); + System.setProperty("TEST_DB_URL", mySqlContainer.getJdbcUrl()); + System.setProperty("TEST_DB_USERNAME", mySqlContainer.getUsername()); + System.setProperty("TEST_DB_PASSWORD", mySqlContainer.getPassword()); + } + + @Override + public void stop() { + super.stop(); + } +} diff --git a/src/test/java/mate/academy/carsharing/service/impl/CarServiceTest.java b/src/test/java/mate/academy/carsharing/service/impl/CarServiceTest.java new file mode 100644 index 0000000..d79fac5 --- /dev/null +++ b/src/test/java/mate/academy/carsharing/service/impl/CarServiceTest.java @@ -0,0 +1,130 @@ +package mate.academy.carsharing.service.impl; + +import static mate.academy.carsharing.util.TestUtils.NOT_VALID_ID; +import static mate.academy.carsharing.util.TestUtils.VALID_ID; +import static mate.academy.carsharing.util.TestUtils.createValidCar; +import static mate.academy.carsharing.util.TestUtils.createValidCarRequestDto; +import static mate.academy.carsharing.util.TestUtils.createValidCarResponseDto; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import mate.academy.carsharing.dto.car.CarResponseDto; +import mate.academy.carsharing.dto.car.CreateCarRequestDto; +import mate.academy.carsharing.exception.EntityNotFoundException; +import mate.academy.carsharing.mapper.CarMapper; +import mate.academy.carsharing.model.Car; +import mate.academy.carsharing.repository.car.CarRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +@ExtendWith(MockitoExtension.class) +public class CarServiceTest { + @Mock + private CarRepository carRepository; + @Mock + private CarMapper carMapper; + @InjectMocks + private CarServiceImpl carService; + + @Test + @DisplayName("save() method works") + public void save_WithValidCarRequestDto_ReturnCarResponseDto() { + CreateCarRequestDto requestDto = createValidCarRequestDto(); + CarResponseDto expected = createValidCarResponseDto(); + Car car = createValidCar(); + when(carMapper.toModel(requestDto)).thenReturn(car); + when(carRepository.save(car)).thenReturn(car); + when(carMapper.toDto(car)).thenReturn(expected); + + CarResponseDto actual = carService.save(requestDto); + + assertEquals(actual, expected); + } + + @Test + @DisplayName("getAll() method works") + public void getAll_WithValidPageable_ReturnCarResponseDtoList() { + Car car = createValidCar(); + CarResponseDto responseDto = createValidCarResponseDto(); + List expected = List.of(responseDto); + Pageable pageable = PageRequest.of(0, 10); + + when(carRepository.findAll(pageable)) + .thenReturn(new PageImpl(List.of(car))); + when(carMapper.toDto(car)).thenReturn(responseDto); + + List actual = carService.getAll(pageable); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("getById() method works") + public void getById_WithValidId_ReturnCarResponseDto() { + Car car = createValidCar(); + CarResponseDto expected = createValidCarResponseDto(); + when(carRepository.findById(VALID_ID)) + .thenReturn(Optional.of(car)); + when(carMapper.toDto(car)).thenReturn(expected); + + CarResponseDto actual = carService.getById(VALID_ID); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("getById() method with invalid 'id' throws EntityNotFoundException") + public void getById_WithInvalidID_ThrowsEntityNotFoundException() { + when(carRepository.findById(NOT_VALID_ID)) + .thenReturn(Optional.empty()); + + assertThrows(EntityNotFoundException.class, () -> carService.getById(NOT_VALID_ID)); + } + + @Test + @DisplayName("updateById() method works") + public void updateById_WithValidIdAndRequestDto_ReturnCarResponseDto() { + Car car = createValidCar(); + CreateCarRequestDto requestDto = createValidCarRequestDto(); + CarResponseDto expected = createValidCarResponseDto(); + when(carRepository.findById(VALID_ID)).thenReturn(Optional.of(car)); + when(carRepository.save(car)).thenReturn(car); + when(carMapper.toModel(requestDto)).thenReturn(car); + when(carMapper.toDto(car)).thenReturn(expected); + + CarResponseDto actual = carService.updateById(VALID_ID, requestDto); + + assertEquals(expected, actual); + verify(carMapper).toModel(requestDto); + verify(carRepository).save(car); + } + + @Test + @DisplayName("updateById() method with invalid 'id' throws EntityNotFoundException") + public void updateById_WithInvalidId_ThrowsEntityNotFoundException() { + CreateCarRequestDto requestDto = createValidCarRequestDto(); + + when(carRepository.findById(NOT_VALID_ID)).thenReturn(Optional.empty()); + + assertThrows(EntityNotFoundException.class, + () -> carService.updateById(NOT_VALID_ID, requestDto)); + } + + @Test + @DisplayName("deleteById() method works") + public void deleteById_WithValidId_ReturnCarResponseDto() { + carService.deleteById(VALID_ID); + verify(carRepository).deleteById(VALID_ID); + } +} diff --git a/src/test/java/mate/academy/carsharing/service/impl/RentalServiceTest.java b/src/test/java/mate/academy/carsharing/service/impl/RentalServiceTest.java new file mode 100644 index 0000000..e0658e6 --- /dev/null +++ b/src/test/java/mate/academy/carsharing/service/impl/RentalServiceTest.java @@ -0,0 +1,342 @@ +package mate.academy.carsharing.service.impl; + +import static mate.academy.carsharing.model.Payment.Status; +import static mate.academy.carsharing.util.TestUtils.VALID_ACTUAL_RETURN_DATE; +import static mate.academy.carsharing.util.TestUtils.VALID_EMAIL; +import static mate.academy.carsharing.util.TestUtils.VALID_ID; +import static mate.academy.carsharing.util.TestUtils.VALID_RENTAL_DATE; +import static mate.academy.carsharing.util.TestUtils.VALID_RETURN_DATE; +import static mate.academy.carsharing.util.TestUtils.createExpiredPayments; +import static mate.academy.carsharing.util.TestUtils.createOverdueRental; +import static mate.academy.carsharing.util.TestUtils.createValidCar; +import static mate.academy.carsharing.util.TestUtils.createValidRental; +import static mate.academy.carsharing.util.TestUtils.createValidRentalRequestDto; +import static mate.academy.carsharing.util.TestUtils.createValidRentalResponseDto; +import static mate.academy.carsharing.util.TestUtils.createValidUser; +import static mate.academy.carsharing.util.TestUtils.returnedRentalResponseDto; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import java.time.LocalDate; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import mate.academy.carsharing.dto.rental.CreateRentalRequestDto; +import mate.academy.carsharing.dto.rental.RentalResponseDto; +import mate.academy.carsharing.dto.rental.RentalSearchParametersDto; +import mate.academy.carsharing.exception.RentalException; +import mate.academy.carsharing.mapper.RentalMapper; +import mate.academy.carsharing.model.Car; +import mate.academy.carsharing.model.Payment; +import mate.academy.carsharing.model.Rental; +import mate.academy.carsharing.model.User; +import mate.academy.carsharing.repository.SpecificationProvider; +import mate.academy.carsharing.repository.car.CarRepository; +import mate.academy.carsharing.repository.payment.PaymentRepository; +import mate.academy.carsharing.repository.rental.RentalRepository; +import mate.academy.carsharing.repository.rental.RentalSpecificationBuilder; +import mate.academy.carsharing.repository.rental.RentalSpecificationProviderManager; +import mate.academy.carsharing.repository.rental.spec.RentalIsActiveSpecificationProvider; +import mate.academy.carsharing.repository.rental.spec.RentalUserIdSpecificationProvider; +import mate.academy.carsharing.repository.user.UserRepository; +import mate.academy.carsharing.service.NotificationService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; + +@ExtendWith(MockitoExtension.class) +public class RentalServiceTest { + @Mock + private RentalRepository rentalRepository; + @Mock + private UserRepository userRepository; + @Mock + private CarRepository carRepository; + @Mock + private RentalMapper rentalMapper; + @Mock + private NotificationService notificationService; + @Mock + private PaymentRepository paymentRepository; + @Mock + private ObjectMapper objectMapper; + @Mock + private RentalSpecificationBuilder rentalSpecificationBuilder; + private final ObjectMapper notMockedMapper = new ObjectMapper(); + @InjectMocks + private RentalServiceImpl rentalService; + + { + notMockedMapper.findAndRegisterModules(); + } + + @Test + @DisplayName("save() method works") + public void save_WithValidCreateRentalRequestDto_ReturnRental() { + CreateRentalRequestDto requestDto = createValidRentalRequestDto(); + Car car = createValidCar(); + User user = createValidUser(); + Rental rental = createValidRental(); + RentalResponseDto expected = createValidRentalResponseDto(); + when(paymentRepository.getAllByUserIdAndPaymentStatus(user.getId(), Status.EXPIRED)) + .thenReturn(List.of()); + when(carRepository.findById(requestDto.carId())) + .thenReturn(Optional.of(car)); + when(userRepository.findById(requestDto.userId())) + .thenReturn(Optional.of(user)); + when(rentalMapper.toModel(requestDto)) + .thenReturn(rental); + ObjectWriter objectWriter = notMockedMapper.writerWithDefaultPrettyPrinter(); + when(objectMapper.writerWithDefaultPrettyPrinter()) + .thenReturn(objectWriter); + doNothing() + .when(notificationService).sendNotification(anyLong(), anyString()); + when(rentalRepository.save(any(Rental.class))) + .thenReturn(rental); + when(rentalMapper.toDto(any(Rental.class))) + .thenReturn(expected); + + RentalResponseDto actualResponse = rentalService.save(requestDto); + + assertEquals(expected, actualResponse); + } + + @Test + @DisplayName("save() method throws RentalException when user has expired payments") + public void save_WithExpiredPayments_ThrowsRentalException() { + CreateRentalRequestDto requestDto = createValidRentalRequestDto(); + List expiredPayments = List.of(createExpiredPayments()); + when(paymentRepository.getAllByUserIdAndPaymentStatus(requestDto.userId(), Status.EXPIRED)) + .thenReturn(expiredPayments); + + Assertions.assertThrows(RentalException.class, () -> rentalService.save(requestDto)); + } + + @Test + @DisplayName("save() method throws RentalException when there is no available car") + public void save_WhenThereIsNoAvailableCar_ThrowsRentalException() { + Car notAvailableCar = createValidCar(); + notAvailableCar.setInventory(0); + + when(carRepository.findById(anyLong())) + .thenReturn(Optional.of(notAvailableCar)); + doNothing() + .when(notificationService).sendNotification(anyLong(), anyString()); + + CreateRentalRequestDto requestDto = createValidRentalRequestDto(); + + Assertions.assertThrows(RentalException.class, () -> rentalService.save(requestDto)); + } + + @Test + @DisplayName("searchRentals() method returns active rentals for specified users") + public void searchRentals_WithValidParams_ReturnActiveRentals() { + Rental notReturnedYetRental = createValidRental(); + + String[] parameterToSearchForTheseUserIds = {VALID_ID.toString()}; + String[] parameterToSearchForActiveRentals = {"true"}; + RentalSearchParametersDto parametersDto = new RentalSearchParametersDto( + parameterToSearchForTheseUserIds, parameterToSearchForActiveRentals); + List> listOfSpecificationProviders = + List.of(new RentalIsActiveSpecificationProvider(), + new RentalUserIdSpecificationProvider()); + RentalSpecificationProviderManager specificationProviderManager = + new RentalSpecificationProviderManager(listOfSpecificationProviders); + RentalSpecificationBuilder specificationBuilder = + new RentalSpecificationBuilder(specificationProviderManager); + Specification specification = specificationBuilder.build(parametersDto); + + when(rentalSpecificationBuilder.build(any(RentalSearchParametersDto.class))) + .thenReturn(specification); + when(rentalRepository.findAll(any(Specification.class), any(Pageable.class))) + .thenReturn(new PageImpl(List.of(notReturnedYetRental))); + + RentalResponseDto responseDtoWithNotReturnedYetRental = createValidRentalResponseDto(); + when(rentalMapper.toDto(any(Rental.class))).thenReturn(responseDtoWithNotReturnedYetRental); + + List expected = List.of(responseDtoWithNotReturnedYetRental); + Pageable pageable = PageRequest.of(0, 20); + List actual = rentalService.searchRentals(parametersDto, pageable); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("searchRentals() method returns nonactive rentals for specified users") + public void searchRentals_WithValidParams_ReturnNonactiveRentals() { + Rental returnedRental = createValidRental(); + returnedRental.setActualReturnDate(VALID_ACTUAL_RETURN_DATE); + RentalResponseDto responseDtoWithReturnedRental = + new RentalResponseDto( + VALID_ID, + VALID_RENTAL_DATE, + VALID_RETURN_DATE, + VALID_ACTUAL_RETURN_DATE, + VALID_ID, + VALID_ID + ); + + String[] parameterToSearchForTheseUserIds = {VALID_ID.toString()}; + String[] parameterToSearchForNonactiveRentals = {"false"}; + RentalSearchParametersDto parametersDto = new RentalSearchParametersDto( + parameterToSearchForTheseUserIds, parameterToSearchForNonactiveRentals); + List> listOfSpecificationProviders = + List.of(new RentalIsActiveSpecificationProvider(), + new RentalUserIdSpecificationProvider()); + RentalSpecificationProviderManager specificationProviderManager = + new RentalSpecificationProviderManager(listOfSpecificationProviders); + RentalSpecificationBuilder specificationBuilder = + new RentalSpecificationBuilder(specificationProviderManager); + Specification specification = specificationBuilder.build(parametersDto); + + when(rentalSpecificationBuilder.build(any(RentalSearchParametersDto.class))) + .thenReturn(specification); + when(rentalRepository.findAll(any(Specification.class), any(Pageable.class))) + .thenReturn(new PageImpl(List.of(returnedRental))); + when(rentalMapper.toDto(any(Rental.class))).thenReturn(responseDtoWithReturnedRental); + + List expected = List.of(responseDtoWithReturnedRental); + Pageable pageable = PageRequest.of(0, 20); + List actual = rentalService.searchRentals(parametersDto, pageable); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("searchRentals() method returns all rentals for specified users") + public void searchRentals_WithValidParams_ReturnAllRentals() { + Rental notReturnedYetRental = createValidRental(); + + Rental returnedRental = createValidRental(); + returnedRental.setActualReturnDate(VALID_ACTUAL_RETURN_DATE); + RentalResponseDto responseDtoWithReturnedRental = + new RentalResponseDto( + VALID_ID, + VALID_RENTAL_DATE, + VALID_RETURN_DATE, + VALID_ACTUAL_RETURN_DATE, + VALID_ID, + VALID_ID + ); + + String[] parameterToSearchForTheseUserIds = {VALID_ID.toString()}; + String[] parameterToSearchForNonactiveRentals = {"false"}; + RentalSearchParametersDto parametersDto = new RentalSearchParametersDto( + parameterToSearchForTheseUserIds, parameterToSearchForNonactiveRentals); + List> listOfSpecificationProviders = + List.of(new RentalIsActiveSpecificationProvider(), + new RentalUserIdSpecificationProvider()); + RentalSpecificationProviderManager specificationProviderManager = + new RentalSpecificationProviderManager(listOfSpecificationProviders); + RentalSpecificationBuilder specificationBuilder = + new RentalSpecificationBuilder(specificationProviderManager); + Specification specification = specificationBuilder.build(parametersDto); + + when(rentalSpecificationBuilder.build(any(RentalSearchParametersDto.class))) + .thenReturn(specification); + when(rentalRepository.findAll(any(Specification.class), any(Pageable.class))) + .thenReturn(new PageImpl(List.of(returnedRental, notReturnedYetRental))); + + RentalResponseDto responseDtoWithNotReturnedYetRental = createValidRentalResponseDto(); + when(rentalMapper.toDto(any(Rental.class))) + .thenReturn(responseDtoWithReturnedRental, responseDtoWithNotReturnedYetRental); + + List expected = + List.of(responseDtoWithReturnedRental, responseDtoWithNotReturnedYetRental); + Pageable pageable = PageRequest.of(0, 20); + List actual = rentalService.searchRentals(parametersDto, pageable); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("getRentalByIdAndUserEmail() method works") + public void getRentalByIdAndUserEmail_ValidRentalIdAndUserEmail_ValidRentalResponseDto() { + Rental rental = createValidRental(); + RentalResponseDto expected = createValidRentalResponseDto(); + when(userRepository.findByEmail(VALID_EMAIL)) + .thenReturn(Optional.of(createValidUser())); + when(rentalRepository.findByIdAndUserId(VALID_ID, VALID_ID)) + .thenReturn(Optional.of(rental)); + when(rentalMapper.toDto(rental)) + .thenReturn(expected); + + RentalResponseDto actual = rentalService.getRentalByIdAndUserEmail(VALID_ID, VALID_EMAIL); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("returnRental() method returns rental with specified id") + public void returnRental_WithValidRentalId_ReturnRental() { + Rental rentalFromDb = createValidRental(); + Rental returnedRental = createValidRental(); + returnedRental.setActualReturnDate(VALID_ACTUAL_RETURN_DATE); + RentalResponseDto expected = returnedRentalResponseDto(); + + when(rentalRepository.findById(VALID_ID)) + .thenReturn(Optional.of(rentalFromDb)); + when(rentalRepository.save(any(Rental.class))) + .thenReturn(returnedRental); + when(rentalMapper.toDto(returnedRental)) + .thenReturn(expected); + + ObjectWriter objectWriter = notMockedMapper.writerWithDefaultPrettyPrinter(); + when(objectMapper.writerWithDefaultPrettyPrinter()) + .thenReturn(objectWriter); + doNothing() + .when(notificationService).sendNotification(anyLong(), anyString()); + + RentalResponseDto actual = rentalService.returnRental(VALID_ID); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("checkOverdueRentals() method sends notifications about every overdue rental") + public void checkOverdueRentals_WithOverdueRentals() { + List overdueRentals = List.of(createOverdueRental(), createOverdueRental()); + when(rentalRepository + .findAllByReturnDateBeforeAndActualReturnDateIsNull(any(LocalDate.class))) + .thenReturn(overdueRentals); + doNothing() + .when(notificationService).sendNotification(anyLong(), anyString()); + + rentalService.checkOverdueRentals(); + + verify(notificationService, times(overdueRentals.size())) + .sendNotification(anyLong(), anyString()); + } + + @Test + @DisplayName("checkOverdueRentals() method sends global notification " + + "when no rentals are overdue") + public void checkOverdueRentals_NoOverdueRentals() { + when(rentalRepository.findAllByReturnDateBeforeAndActualReturnDateIsNull( + any(LocalDate.class))) + .thenReturn(Collections.emptyList()); + doNothing() + .when(notificationService).sendGlobalNotification(anyString()); + + rentalService.checkOverdueRentals(); + + verify(notificationService, times(1)) + .sendGlobalNotification(anyString()); + } +} diff --git a/src/test/java/mate/academy/carsharing/service/impl/StripePaymentServiceTest.java b/src/test/java/mate/academy/carsharing/service/impl/StripePaymentServiceTest.java new file mode 100644 index 0000000..80dfac6 --- /dev/null +++ b/src/test/java/mate/academy/carsharing/service/impl/StripePaymentServiceTest.java @@ -0,0 +1,317 @@ +package mate.academy.carsharing.service.impl; + +import static mate.academy.carsharing.util.TestUtils.NOT_VALID_ID; +import static mate.academy.carsharing.util.TestUtils.VALID_EMAIL; +import static mate.academy.carsharing.util.TestUtils.VALID_ID; +import static mate.academy.carsharing.util.TestUtils.VALID_SESSION_ID; +import static mate.academy.carsharing.util.TestUtils.VALID_SESSION_URL; +import static mate.academy.carsharing.util.TestUtils.createValidPayment; +import static mate.academy.carsharing.util.TestUtils.createValidPaymentResponseDto; +import static mate.academy.carsharing.util.TestUtils.createValidRental; +import static mate.academy.carsharing.util.TestUtils.createValidUser; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.stripe.exception.StripeException; +import com.stripe.model.checkout.Session; +import java.math.BigDecimal; +import java.net.MalformedURLException; +import java.util.List; +import java.util.Optional; +import mate.academy.carsharing.dto.payment.CreatePaymentRequestDto; +import mate.academy.carsharing.dto.payment.PaymentResponseDto; +import mate.academy.carsharing.dto.payment.PaymentSearchParametersDto; +import mate.academy.carsharing.exception.EntityNotFoundException; +import mate.academy.carsharing.mapper.PaymentMapper; +import mate.academy.carsharing.model.Payment; +import mate.academy.carsharing.model.Rental; +import mate.academy.carsharing.model.Role; +import mate.academy.carsharing.model.User; +import mate.academy.carsharing.repository.SpecificationProvider; +import mate.academy.carsharing.repository.payment.PaymentRepository; +import mate.academy.carsharing.repository.payment.PaymentSpecificationBuilder; +import mate.academy.carsharing.repository.payment.PaymentSpecificationProviderManager; +import mate.academy.carsharing.repository.payment.spec.PaymentUserIdSpecificationProvider; +import mate.academy.carsharing.repository.rental.RentalRepository; +import mate.academy.carsharing.repository.role.RoleRepository; +import mate.academy.carsharing.repository.user.UserRepository; +import mate.academy.carsharing.service.NotificationService; +import mate.academy.carsharing.stripe.StripeSessionProvider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; + +@ExtendWith(MockitoExtension.class) +public class StripePaymentServiceTest { + @Mock + private PaymentRepository paymentRepository; + @Mock + private PaymentMapper paymentMapper; + @Mock + private RentalRepository rentalRepository; + @Mock + private NotificationService notificationService; + @Mock + private StripeSessionProvider stripeSessionProvider; + @Mock + private UserRepository userRepository; + @Mock + private RoleRepository roleRepository; + @Mock + private PaymentSpecificationBuilder paymentSpecificationBuilder; + @InjectMocks + private StripePaymentServiceImpl stripeService; + + @Test + @DisplayName("save() method works") + void save_WithValidCreatePaymentRequestDto_ReturnPaymentResponseDto() + throws MalformedURLException, StripeException { + Rental rental = createValidRental(); + CreatePaymentRequestDto requestDto = new CreatePaymentRequestDto(rental.getId()); + Session mockSession = mock(Session.class); + when(stripeSessionProvider.createStripeSession(any(BigDecimal.class), any(String.class))) + .thenReturn(mockSession); + when(rentalRepository.findById(VALID_ID)) + .thenReturn(Optional.of(rental)); + when(paymentRepository + .getSumByRentalAndPaymentStatus(any(Rental.class), eq(Payment.Status.PAID))) + .thenReturn(BigDecimal.ZERO); + when(mockSession.getId()) + .thenReturn(VALID_SESSION_ID); + when(mockSession.getUrl()) + .thenReturn(VALID_SESSION_URL); + doNothing() + .when(notificationService).sendNotification(anyLong(), anyString()); + Payment payment = createValidPayment(); + when(paymentRepository.save(any(Payment.class))) + .thenReturn(payment); + when(paymentMapper.toDto(any(Payment.class))) + .thenReturn(createValidPaymentResponseDto()); + + PaymentResponseDto actualResponse = stripeService.save(requestDto); + + Assertions.assertNotNull(actualResponse); + Assertions.assertEquals(VALID_SESSION_ID, actualResponse.sessionId()); + Assertions.assertEquals(VALID_SESSION_URL, actualResponse.sessionUrl()); + verify(notificationService).sendNotification(eq(VALID_ID), anyString()); + } + + @Test + @DisplayName("search() method returns payments for specified users") + public void search_WithValidPaymentSearchParametersDto_ReturnListOfPaymentResponseDto() + throws MalformedURLException { + String[] parameterToSearchForTheseUserIds = {VALID_ID.toString()}; + PaymentSearchParametersDto searchParametersDto = new PaymentSearchParametersDto( + parameterToSearchForTheseUserIds); + List> listOfSpecificationProviders = + List.of(new PaymentUserIdSpecificationProvider()); + PaymentSpecificationProviderManager specificationProviderManager = + new PaymentSpecificationProviderManager(listOfSpecificationProviders); + PaymentSpecificationBuilder specificationBuilder = + new PaymentSpecificationBuilder(specificationProviderManager); + Specification specification = specificationBuilder.build(searchParametersDto); + + User validUser = createValidUser(); + Role roleManager = new Role(); + roleManager.setName(Role.RoleName.ROLE_MANAGER); + Payment validPayment = createValidPayment(); + PaymentResponseDto expected = createValidPaymentResponseDto(); + when(paymentSpecificationBuilder.build(any(PaymentSearchParametersDto.class))) + .thenReturn(specification); + when(userRepository.findByEmail(anyString())) + .thenReturn(Optional.of(validUser)); + when(roleRepository.findByName(Role.RoleName.ROLE_MANAGER)) + .thenReturn(Optional.of(roleManager)); + when(paymentRepository.findAll(any(Specification.class), any(Pageable.class))) + .thenReturn(new PageImpl(List.of(validPayment))); + when(paymentMapper.toDto(validPayment)) + .thenReturn(expected); + + Pageable pageable = PageRequest.of(0, 20); + List actual = stripeService.search(VALID_EMAIL, searchParametersDto, + pageable); + + Assertions.assertEquals(List.of(expected), actual); + } + + @Test + @DisplayName("processSuccessfulPayment() method work") + public void processSuccessfulPayment_WithValidSessionId_ReturnPaymentResponseDto() + throws MalformedURLException { + Payment payment = createValidPayment(); + payment.setStatus(Payment.Status.PAID); + when(paymentRepository.findBySessionId(VALID_SESSION_ID)) + .thenReturn(Optional.of(createValidPayment())); + Session mockSession = mock(Session.class); + try { + when(stripeSessionProvider.retrieveSession(VALID_SESSION_ID)) + .thenReturn(mockSession); + } catch (StripeException e) { + fail("retrieveSession error!"); + } + when(mockSession.getPaymentStatus()) + .thenReturn("paid"); + when(paymentRepository.save(payment)) + .thenReturn(payment); + PaymentResponseDto expected = new PaymentResponseDto( + VALID_ID, + VALID_ID, + VALID_ID, + Payment.Status.PAID.name(), + Payment.Type.PAYMENT.name(), + VALID_SESSION_URL, + VALID_SESSION_ID, + BigDecimal.TEN + ); + when(paymentMapper.toDto(payment)) + .thenReturn(expected); + + PaymentResponseDto actual = stripeService.processSuccessfulPayment(VALID_SESSION_ID); + + Assertions.assertEquals(expected, actual); + verify(notificationService).sendNotification(any(Long.class), any(String.class)); + } + + @Test + @DisplayName("processCanceledPayment() method work") + public void processCanceledPayment_WithValidSessionId_ReturnPaymentResponseDto() + throws MalformedURLException { + Payment payment = createValidPayment(); + payment.setStatus(Payment.Status.CANCEL); + PaymentResponseDto expected = new PaymentResponseDto( + VALID_ID, + VALID_ID, + VALID_ID, + Payment.Status.CANCEL.name(), + Payment.Type.PAYMENT.name(), + VALID_SESSION_URL, + VALID_SESSION_ID, + BigDecimal.TEN + ); + when(paymentRepository.findBySessionId(VALID_SESSION_ID)) + .thenReturn(Optional.of(createValidPayment())); + doNothing() + .when(notificationService).sendNotification(anyLong(), anyString()); + when(paymentRepository.save(payment)) + .thenReturn(payment); + when(paymentMapper.toDto(payment)) + .thenReturn(expected); + + PaymentResponseDto actual = stripeService.processCanceledPayment(VALID_SESSION_ID); + + Assertions.assertEquals(expected, actual); + verify(notificationService).sendNotification(any(Long.class), any(String.class)); + } + + @Test + @DisplayName("renewPaymentSession() method successfully renews a session") + void renewPaymentSession_WithValidParams_ThenSuccessfulRenewSession() + throws StripeException, MalformedURLException { + Payment payment = createValidPayment(); + payment.setStatus(Payment.Status.CANCEL); + Role roleManager = new Role(); + roleManager.setName(Role.RoleName.ROLE_MANAGER); + Session mockSession = mock(Session.class); + when(mockSession.getId()) + .thenReturn("new_stripe_session_id"); + when(mockSession.getUrl()) + .thenReturn("http://new.payment.url"); + PaymentResponseDto expected = new PaymentResponseDto( + VALID_ID, + VALID_ID, + VALID_ID, + Payment.Status.PENDING.name(), + Payment.Type.PAYMENT.name(), + "http://new.payment.url", + "new_stripe_session_id", + BigDecimal.TEN + ); + when(paymentRepository.findById(anyLong())) + .thenReturn(Optional.of(payment)); + when(userRepository.findByEmail(anyString())) + .thenReturn(Optional.of(createValidUser())); + when(roleRepository.findByName(Role.RoleName.ROLE_MANAGER)) + .thenReturn(Optional.of(roleManager)); + + when(stripeSessionProvider.createStripeSession(any(BigDecimal.class), anyString())) + .thenReturn(mockSession); + when(paymentRepository.save(any(Payment.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + when(paymentMapper.toDto(any(Payment.class))) + .thenReturn(expected); + + PaymentResponseDto actualResponse = + stripeService.renewPaymentSession(VALID_ID, VALID_EMAIL); + + Assertions.assertNotNull(actualResponse); + Assertions.assertEquals(expected, actualResponse); + Assertions.assertEquals("new_stripe_session_id", actualResponse.sessionId()); + Assertions.assertEquals("http://new.payment.url", actualResponse.sessionUrl()); + } + + @Test + @DisplayName("renewPaymentSession() method with invalid 'paymentId' " + + "throws EntityNotFoundException") + void renewPaymentSession_WitInvalidPaymentId_ThrowEntityNotFoundException() { + when(paymentRepository.findById(NOT_VALID_ID)) + .thenReturn(Optional.empty()); + + Assertions.assertThrows(EntityNotFoundException.class, + () -> stripeService.renewPaymentSession(NOT_VALID_ID, VALID_EMAIL)); + } + + @Test + @DisplayName("renewPaymentSession() method with invalid 'email' " + + "throws EntityNotFoundException") + void renewPaymentSession_WitInvalidEmail_ThrowEntityNotFoundException() + throws MalformedURLException { + Long validPaymentId = VALID_ID; + String email = "invalid@email.com"; + Payment payment = createValidPayment(); + + when(paymentRepository.findById(validPaymentId)) + .thenReturn(Optional.of(payment)); + when(userRepository.findByEmail(anyString())) + .thenReturn(Optional.empty()); + + Assertions.assertThrows(RuntimeException.class, + () -> stripeService.renewPaymentSession(validPaymentId, email)); + } + + @Test + @DisplayName("checkExpiredStripeSessions() methode updates status for expired sessions") + void checkExpiredStripeSessions_UpdateExpiredSessionStatus() + throws StripeException, MalformedURLException { + List pendingPayments = List.of(createValidPayment()); + Session expiredSession = mock(Session.class); + when(expiredSession.getStatus()) + .thenReturn("expired"); + when(paymentRepository.findAllByStatus(Payment.Status.PENDING)) + .thenReturn(pendingPayments); + when(stripeSessionProvider.retrieveSession(anyString())) + .thenReturn(expiredSession); + when(paymentRepository.save(any(Payment.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + stripeService.checkExpiredStripeSessions(); + + for (Payment payment : pendingPayments) { + Assertions.assertEquals(Payment.Status.EXPIRED, payment.getStatus()); + } + } +} diff --git a/src/test/java/mate/academy/carsharing/service/impl/TelegramNotificationServiceTest.java b/src/test/java/mate/academy/carsharing/service/impl/TelegramNotificationServiceTest.java new file mode 100644 index 0000000..31f8ba8 --- /dev/null +++ b/src/test/java/mate/academy/carsharing/service/impl/TelegramNotificationServiceTest.java @@ -0,0 +1,98 @@ +package mate.academy.carsharing.service.impl; + +import static mate.academy.carsharing.util.TestUtils.createValidUser; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import mate.academy.carsharing.model.TelegramUserInfo; +import mate.academy.carsharing.model.User; +import mate.academy.carsharing.repository.telegram.TelegramUserInfoRepository; +import mate.academy.carsharing.telegram.TelegramMessageEvent; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +@ExtendWith(MockitoExtension.class) +public class TelegramNotificationServiceTest { + @Mock + private ApplicationEventPublisher eventPublisher; + @Mock + private TelegramUserInfoRepository telegramUserInfoRepository; + @InjectMocks + private TelegramNotificationServiceImpl telegramNotificationService; + + @Test + @DisplayName("sendNotification() method sends message to a specific user") + void sendNotification_WithValidUserId_SendNotificationToSpecificUser() { + User validUser = createValidUser(); + TelegramUserInfo userInfo = new TelegramUserInfo(); + userInfo.setChatId(123L); + userInfo.setUser(validUser); + String message = "Test message"; + when(telegramUserInfoRepository.findByUserId(validUser.getId())) + .thenReturn(Optional.of(userInfo)); + + telegramNotificationService.sendNotification(validUser.getId(), message); + + verify(eventPublisher) + .publishEvent(new TelegramMessageEvent(userInfo.getChatId(), message)); + } + + @Test + @DisplayName("sendNotification() method does nothing if user not found") + void sendNotification_WithNotValidUserId_DoNotSendMessage() { + Long userId = 1L; + String message = "Test message"; + when(telegramUserInfoRepository.findByUserId(userId)) + .thenReturn(Optional.empty()); + + telegramNotificationService.sendNotification(userId, message); + + verify(eventPublisher, never()) + .publishEvent(Mockito.any(TelegramMessageEvent.class)); + } + + @Test + @DisplayName("sendGlobalNotification() method sends messages to all users") + void sendGlobalNotification_SendMessageToAllUsers() { + User validUser = createValidUser(); + TelegramUserInfo userInfo01 = new TelegramUserInfo(); + userInfo01.setId(1L); + userInfo01.setChatId(123L); + userInfo01.setUser(validUser); + TelegramUserInfo userInfo02 = new TelegramUserInfo(); + userInfo02.setId(1L); + userInfo02.setChatId(456L); + userInfo02.setUser(createValidUser()); + String message = "Global message"; + + List userList = List.of(userInfo01, userInfo02); + Mockito.when(telegramUserInfoRepository.findAll()).thenReturn(userList); + + telegramNotificationService.sendGlobalNotification(message); + + userList.forEach(user -> Mockito.verify(eventPublisher) + .publishEvent(new TelegramMessageEvent(user.getChatId(), message))); + } + + @Test + @DisplayName("sendGlobalNotification() method does nothing if users list is empty") + void sendGlobalNotification_NoUsers() { + String message = "Global message"; + Mockito.when(telegramUserInfoRepository.findAll()) + .thenReturn(List.of()); + + telegramNotificationService.sendGlobalNotification(message); + + Mockito.verify(eventPublisher, Mockito.never()) + .publishEvent(Mockito.any(TelegramMessageEvent.class)); + } +} diff --git a/src/test/java/mate/academy/carsharing/service/impl/TelegramUserServiceTest.java b/src/test/java/mate/academy/carsharing/service/impl/TelegramUserServiceTest.java new file mode 100644 index 0000000..2512381 --- /dev/null +++ b/src/test/java/mate/academy/carsharing/service/impl/TelegramUserServiceTest.java @@ -0,0 +1,75 @@ +package mate.academy.carsharing.service.impl; + +import static mate.academy.carsharing.util.TestUtils.NOT_VALID_EMAIL; +import static mate.academy.carsharing.util.TestUtils.REGISTRATION_SUCCESS; +import static mate.academy.carsharing.util.TestUtils.VALID_CHAT_ID; +import static mate.academy.carsharing.util.TestUtils.VALID_EMAIL; +import static mate.academy.carsharing.util.TestUtils.VALID_ID; +import static mate.academy.carsharing.util.TestUtils.createValidTelegramUserState; +import static mate.academy.carsharing.util.TestUtils.createValidUser; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import mate.academy.carsharing.repository.telegram.TelegramUserInfoRepository; +import mate.academy.carsharing.repository.user.UserRepository; +import mate.academy.carsharing.telegram.TelegramMessageEvent; +import mate.academy.carsharing.telegram.TelegramUserState; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +@ExtendWith(MockitoExtension.class) +public class TelegramUserServiceTest { + @Mock + private UserRepository userRepository; + @Mock + private TelegramUserInfoRepository telegramUserInfoRepository; + @Mock + private ApplicationEventPublisher eventPublisher; + @InjectMocks + private TelegramUserServiceImpl telegramUserService; + + @Test + @DisplayName("registerNewUser() method with invalid 'email' sends error message") + public void registerNewUser_WithValidChatIdAndInvalidEmail_SendErrorMessage() { + telegramUserService.registerNewUser(VALID_CHAT_ID, NOT_VALID_EMAIL); + + verify(eventPublisher).publishEvent( + new TelegramMessageEvent(VALID_CHAT_ID, + "Not valid email, please send again")); + } + + @Test + @DisplayName("registerNewUser() method registers a new user and sends success message") + public void registerNewUser_WithValidChatIdAndUsername_SendSuccessRegistrationMessage() { + TelegramUserState telegramUserState = new TelegramUserState(VALID_ID, VALID_EMAIL, false); + telegramUserService.addUserState(VALID_CHAT_ID, telegramUserState); + + when(telegramUserInfoRepository.findByChatId(VALID_CHAT_ID)) + .thenReturn(Optional.empty()); + when(userRepository.findByEmail(VALID_EMAIL)) + .thenReturn(Optional.of(createValidUser())); + + telegramUserService.registerNewUser(VALID_CHAT_ID, VALID_EMAIL); + + verify(eventPublisher) + .publishEvent(new TelegramMessageEvent(VALID_CHAT_ID, REGISTRATION_SUCCESS)); + } + + @Test + @DisplayName("getUserState() method works") + void getUserState_ValidChatId_ValidTelegramUserState() { + TelegramUserState expected = createValidTelegramUserState(); + telegramUserService.addUserState(VALID_CHAT_ID, expected); + + TelegramUserState actual = telegramUserService.getUserState(VALID_CHAT_ID); + + Assertions.assertEquals(expected, actual); + } +} diff --git a/src/test/java/mate/academy/carsharing/service/impl/UserServiceTest.java b/src/test/java/mate/academy/carsharing/service/impl/UserServiceTest.java new file mode 100644 index 0000000..c5f0534 --- /dev/null +++ b/src/test/java/mate/academy/carsharing/service/impl/UserServiceTest.java @@ -0,0 +1,176 @@ +package mate.academy.carsharing.service.impl; + +import static mate.academy.carsharing.util.TestUtils.NOT_VALID_EMAIL; +import static mate.academy.carsharing.util.TestUtils.VALID_EMAIL; +import static mate.academy.carsharing.util.TestUtils.VALID_FIRST_NAME; +import static mate.academy.carsharing.util.TestUtils.VALID_ID; +import static mate.academy.carsharing.util.TestUtils.VALID_NEW_LAST_NAME; +import static mate.academy.carsharing.util.TestUtils.VALID_ROLE; +import static mate.academy.carsharing.util.TestUtils.createValidRole; +import static mate.academy.carsharing.util.TestUtils.createValidUser; +import static mate.academy.carsharing.util.TestUtils.createValidUserRegistrationRequestDto; +import static mate.academy.carsharing.util.TestUtils.createValidUserResponseDto; +import static mate.academy.carsharing.util.TestUtils.createValidUserUpdateInfoRequestDto; +import static mate.academy.carsharing.util.TestUtils.createValidUserUpdateRoleRequestDto; +import static mate.academy.carsharing.util.TestUtils.createValidUserWithRoleResponseDto; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.Set; +import mate.academy.carsharing.dto.user.UserRegistrationRequestDto; +import mate.academy.carsharing.dto.user.UserResponseDto; +import mate.academy.carsharing.dto.user.UserResponseDtoWithRoles; +import mate.academy.carsharing.dto.user.UserUpdateInfoRequestDto; +import mate.academy.carsharing.dto.user.UserUpdateRoleRequestDto; +import mate.academy.carsharing.exception.EntityNotFoundException; +import mate.academy.carsharing.exception.RegistrationException; +import mate.academy.carsharing.mapper.UserMapper; +import mate.academy.carsharing.model.Role; +import mate.academy.carsharing.model.User; +import mate.academy.carsharing.repository.role.RoleRepository; +import mate.academy.carsharing.repository.user.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; + +@ExtendWith(MockitoExtension.class) +public class UserServiceTest { + @Mock + private UserRepository userRepository; + @Mock + private UserMapper userMapper; + @Mock + private RoleRepository roleRepository; + @Mock + private PasswordEncoder passwordEncoder; + @InjectMocks + private UserServiceImpl userService; + + @Test + @DisplayName("register() method works") + public void register_WithValidUserRegistrationRequestDto_ReturnUserResponseDto() { + UserRegistrationRequestDto requestDto = createValidUserRegistrationRequestDto(); + User newUser = createValidUser(); + UserResponseDto expected = createValidUserResponseDto(); + when(userRepository.findByEmail(requestDto.email())) + .thenReturn(Optional.empty()); + when(userMapper.toModel(requestDto)) + .thenReturn(newUser); + when(passwordEncoder.encode(requestDto.password())) + .thenReturn("HashedPassword"); + when(roleRepository.findByName(any(Role.RoleName.class))) + .thenReturn(Optional.of(createValidRole(VALID_ID, Role.RoleName.ROLE_CUSTOMER))); + User savedUser = createValidUser(); + when(userRepository.save(newUser)) + .thenReturn(savedUser); + when(userMapper.toDto(savedUser)) + .thenReturn(expected); + + UserResponseDto actual = userService.register(requestDto); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("updateUserRole() method works") + public void updateUserRole_WithValidUserUpdateRoleRequestDto_ReturnUserResponseDtoWithRoles() { + User user = createValidUser(); + Role role = createValidRole(VALID_ID, Role.RoleName.ROLE_CUSTOMER); + user.setRoles(Set.of(role)); + UserUpdateRoleRequestDto requestDto = createValidUserUpdateRoleRequestDto(); + UserResponseDtoWithRoles expected = createValidUserWithRoleResponseDto(); + when(userRepository.findById(VALID_ID)) + .thenReturn(Optional.of(createValidUser())); + when(roleRepository.findByName(VALID_ROLE)) + .thenReturn(Optional.of(role)); + when(userRepository.save(user)) + .thenReturn(user); + when(userMapper.toDtoWithRoles(user)) + .thenReturn(expected); + + UserResponseDtoWithRoles actual = userService.updateUserRole(VALID_ID, requestDto); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("updateUserInfo() method works") + public void updateUserInfo_WithValidEmailAndUserUpdateInfoRequestDto_ReturnUserResponseDto() { + User user = createValidUser(); + UserResponseDto expected = new UserResponseDto( + VALID_ID, + VALID_EMAIL, + VALID_FIRST_NAME, + VALID_NEW_LAST_NAME + ); + + UserUpdateInfoRequestDto requestDto = createValidUserUpdateInfoRequestDto(); + when(userRepository.findByEmail(VALID_EMAIL)) + .thenReturn(Optional.of(user)); + when(userRepository.save(user)) + .thenReturn(user); + when(userMapper.toDto(user)) + .thenReturn(expected); + + UserResponseDto actual = userService.updateUserInfo(VALID_EMAIL, requestDto); + + verify(userMapper).toDto(user); + assertEquals(expected, actual); + } + + @Test + @DisplayName("getUserInfo() method works") + public void getUserInfo_WithValidEmail_ReturnUserResponseDtoWithRoles() { + User user = createValidUser(); + UserResponseDtoWithRoles expected = createValidUserWithRoleResponseDto(); + when(userRepository.findByEmail(VALID_EMAIL)) + .thenReturn(Optional.of(user)); + when(userMapper.toDtoWithRoles(user)) + .thenReturn(expected); + + UserResponseDtoWithRoles actual = userService.getUserInfo(VALID_EMAIL); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("register() method for already registered user throws RegistrationException") + public void register_WithAlreadyRegisteredUser_ThrowRegistrationException() { + UserRegistrationRequestDto requestDto = createValidUserRegistrationRequestDto(); + when(userRepository.findByEmail(requestDto.email())) + .thenReturn(Optional.of(createValidUser())); + + assertThrows(RegistrationException.class, () -> userService.register(requestDto)); + } + + @Test + @DisplayName("updateUserRole() method with invalid 'Role' throws EntityNotFoundException") + public void updateUserRole_WithNotValidRole_ThrowEntityNotFoundException() { + User user = createValidUser(); + when(userRepository.findById(VALID_ID)) + .thenReturn(Optional.of(user)); + when(roleRepository.findByName(any(Role.RoleName.class))) + .thenReturn(Optional.empty()); + + assertThrows(EntityNotFoundException.class, () -> userService.updateUserRole(VALID_ID, + new UserUpdateRoleRequestDto( + createValidRole(VALID_ID, Role.RoleName.ROLE_CUSTOMER)))); + } + + @Test + @DisplayName("getUserInfo() method with invalid 'email' throws EntityNotFoundException") + void getUserInfo_WithNotValidEmail_ThrowEntityNotFoundException() { + when(userRepository.findByEmail(NOT_VALID_EMAIL)) + .thenReturn(Optional.empty()); + + assertThrows(EntityNotFoundException.class, () -> userService.getUserInfo(NOT_VALID_EMAIL)); + } +} diff --git a/src/test/java/mate/academy/carsharing/util/TestUtils.java b/src/test/java/mate/academy/carsharing/util/TestUtils.java new file mode 100644 index 0000000..ac6b866 --- /dev/null +++ b/src/test/java/mate/academy/carsharing/util/TestUtils.java @@ -0,0 +1,243 @@ +package mate.academy.carsharing.util; + +import java.math.BigDecimal; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.LocalDate; +import java.util.Set; +import mate.academy.carsharing.dto.car.CarResponseDto; +import mate.academy.carsharing.dto.car.CreateCarRequestDto; +import mate.academy.carsharing.dto.payment.PaymentResponseDto; +import mate.academy.carsharing.dto.rental.CreateRentalRequestDto; +import mate.academy.carsharing.dto.rental.RentalResponseDto; +import mate.academy.carsharing.dto.user.UserRegistrationRequestDto; +import mate.academy.carsharing.dto.user.UserResponseDto; +import mate.academy.carsharing.dto.user.UserResponseDtoWithRoles; +import mate.academy.carsharing.dto.user.UserUpdateInfoRequestDto; +import mate.academy.carsharing.dto.user.UserUpdateRoleRequestDto; +import mate.academy.carsharing.model.Car; +import mate.academy.carsharing.model.Payment; +import mate.academy.carsharing.model.Rental; +import mate.academy.carsharing.model.Role; +import mate.academy.carsharing.model.User; +import mate.academy.carsharing.telegram.TelegramUserState; + +public final class TestUtils { + public static final Long VALID_ID = 1L; + public static final Long NOT_VALID_ID = -1L; + public static final String VALID_MODEL = "Valid Model"; + public static final String VALID_BRAND = "Valid Brand"; + public static final Car.Type VALID_TYPE = Car.Type.SUV; + public static final Integer VALID_INVENTORY = 10; + public static final BigDecimal VALID_DAILY_FEE = BigDecimal.TEN; + public static final String VALID_EMAIL = "test@email.com"; + public static final String NOT_VALID_EMAIL = "NOT VALID EMAIL"; + public static final String VALID_FIRST_NAME = "First Name"; + public static final String VALID_LAST_NAME = "Last Name"; + public static final String VALID_PASSWORD = "Password"; + public static final LocalDate VALID_RENTAL_DATE = LocalDate.now(); + public static final LocalDate VALID_RETURN_DATE = LocalDate.now().plusDays(5); + public static final LocalDate VALID_ACTUAL_RETURN_DATE = LocalDate.now().plusDays(4); + public static final String VALID_SESSION_ID = "stripe_session_id"; + public static final String VALID_SESSION_URL = "http://payment.url"; + public static final Long VALID_CHAT_ID = 123456L; + public static final String REGISTRATION_SUCCESS = """ + Registration success. I know next commands: '/checkRentals' + """; + public static final String VALID_NEW_LAST_NAME = "New Last Name"; + public static final Role.RoleName VALID_ROLE = Role.RoleName.ROLE_CUSTOMER; + + private TestUtils() { + } + + public static Car createValidCar() { + Car car = new Car(); + car.setId(VALID_ID); + car.setModel(VALID_MODEL); + car.setBrand(VALID_BRAND); + car.setType(VALID_TYPE); + car.setInventory(VALID_INVENTORY); + car.setDailyFee(VALID_DAILY_FEE); + return car; + } + + public static CreateCarRequestDto createValidCarRequestDto() { + return new CreateCarRequestDto( + VALID_MODEL, + VALID_BRAND, + VALID_TYPE, + VALID_INVENTORY, + VALID_DAILY_FEE + ); + } + + public static CarResponseDto createValidCarResponseDto() { + CarResponseDto carResponseDto = new CarResponseDto(); + carResponseDto.setId(VALID_ID); + carResponseDto.setModel(VALID_MODEL); + carResponseDto.setBrand(VALID_BRAND); + carResponseDto.setType(VALID_TYPE); + carResponseDto.setInventory(VALID_INVENTORY); + carResponseDto.setDailyFee(VALID_DAILY_FEE); + return carResponseDto; + } + + public static User createValidUser() { + User user = new User(); + user.setId(VALID_ID); + user.setEmail(VALID_EMAIL); + user.setFirstName(VALID_FIRST_NAME); + user.setLastName(VALID_LAST_NAME); + user.setPassword(VALID_PASSWORD); + return user; + } + + public static Rental createValidRental() { + Rental rental = new Rental(); + rental.setId(VALID_ID); + rental.setRentalDate(VALID_RENTAL_DATE); + rental.setReturnDate(VALID_RETURN_DATE); + rental.setCar(createValidCar()); + rental.setUser(createValidUser()); + return rental; + } + + public static CreateRentalRequestDto createValidRentalRequestDto() { + return new CreateRentalRequestDto( + VALID_RENTAL_DATE, + VALID_RETURN_DATE, + VALID_ID, + VALID_ID + ); + } + + public static RentalResponseDto createValidRentalResponseDto() { + return new RentalResponseDto( + VALID_ID, + VALID_RENTAL_DATE, + VALID_RETURN_DATE, + null, + VALID_ID, + VALID_ID + ); + } + + public static Payment createExpiredPayments() { + Payment payment = new Payment(); + payment.setId(VALID_ID); + payment.setRental(createValidRental()); + try { + payment.setSessionUrl(new URL("http://localhost:8080/")); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + payment.setStatus(Payment.Status.EXPIRED); + payment.setSessionId("SESSION ID"); + payment.setType(Payment.Type.PAYMENT); + payment.setAmountToPay(BigDecimal.TEN); + return payment; + } + + public static RentalResponseDto returnedRentalResponseDto() { + return new RentalResponseDto( + VALID_ID, + VALID_RENTAL_DATE, + VALID_RETURN_DATE, + VALID_ACTUAL_RETURN_DATE, + VALID_ID, + VALID_ID + ); + } + + public static Rental createOverdueRental() { + Rental rental = new Rental(); + rental.setId(VALID_ID); + rental.setRentalDate(LocalDate.now().minusDays(5)); + rental.setReturnDate(VALID_RETURN_DATE); + rental.setCar(createValidCar()); + rental.setUser(createValidUser()); + rental.setDeleted(false); + return rental; + } + + public static Payment createValidPayment() throws MalformedURLException { + Payment payment = new Payment(); + payment.setId(VALID_ID); + payment.setRental(createValidRental()); + payment.setSessionUrl(new URL(VALID_SESSION_URL)); + payment.setStatus(Payment.Status.PENDING); + payment.setSessionId(VALID_SESSION_ID); + payment.setType(Payment.Type.PAYMENT); + payment.setAmountToPay(BigDecimal.TEN); + return payment; + } + + public static PaymentResponseDto createValidPaymentResponseDto() { + return new PaymentResponseDto( + VALID_ID, + VALID_ID, + VALID_ID, + Payment.Status.PENDING.name(), + Payment.Type.PAYMENT.name(), + VALID_SESSION_URL, + VALID_SESSION_ID, + BigDecimal.TEN + ); + } + + public static TelegramUserState createValidTelegramUserState() { + return new TelegramUserState( + VALID_ID, + VALID_EMAIL, + true + ); + } + + public static UserRegistrationRequestDto createValidUserRegistrationRequestDto() { + return new UserRegistrationRequestDto( + VALID_EMAIL, + VALID_PASSWORD, + VALID_PASSWORD, + VALID_FIRST_NAME, + VALID_LAST_NAME + ); + } + + public static UserResponseDto createValidUserResponseDto() { + return new UserResponseDto( + VALID_ID, + VALID_EMAIL, + VALID_FIRST_NAME, + VALID_LAST_NAME + ); + } + + public static UserUpdateRoleRequestDto createValidUserUpdateRoleRequestDto() { + return new UserUpdateRoleRequestDto(createValidRole(VALID_ID, Role.RoleName.ROLE_CUSTOMER)); + } + + public static Role createValidRole(Long roleId, Role.RoleName roleName) { + Role role = new Role(); + role.setId(roleId); + role.setName(roleName); + return role; + } + + public static UserResponseDtoWithRoles createValidUserWithRoleResponseDto() { + return new UserResponseDtoWithRoles( + VALID_ID, + VALID_EMAIL, + VALID_FIRST_NAME, + VALID_LAST_NAME, + Set.of(createValidRole(VALID_ID, Role.RoleName.ROLE_CUSTOMER)) + ); + } + + public static UserUpdateInfoRequestDto createValidUserUpdateInfoRequestDto() { + return new UserUpdateInfoRequestDto( + VALID_PASSWORD, + VALID_FIRST_NAME, + VALID_NEW_LAST_NAME + ); + } +} diff --git a/src/test/resources/application-h2db.properties b/src/test/resources/application-h2db.properties index 3b86530..8b143df 100644 --- a/src/test/resources/application-h2db.properties +++ b/src/test/resources/application-h2db.properties @@ -1,2 +1,6 @@ -spring.datasource.url=jdbc:h2:mem:test_db +# for tests use H2 in MySQL mode +spring.datasource.url=jdbc:h2:mem:test_db?createDatabaseIfNotExist=true&serverTimezone=UTC;\ + DATABASE_TO_LOWER=TRUE;MODE=MySQL; spring.datasource.driverClassName=org.h2.Driver +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 51df8ad..bdd27b2 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1,4 +1,4 @@ -spring.profiles.active=testcontainers +spring.profiles.active=testcontainers, telegram_bot, stripe #spring.profiles.active=localdb #spring.profiles.active=h2db diff --git a/src/test/resources/db/changelog/changelog-test.yaml b/src/test/resources/db/changelog/changelog-test.yaml index 8e2a9f0..459fc7b 100644 --- a/src/test/resources/db/changelog/changelog-test.yaml +++ b/src/test/resources/db/changelog/changelog-test.yaml @@ -13,3 +13,9 @@ databaseChangeLog: file: db/changelog/changes/06-create-rentals-table.yaml - include: file: db/changelog/changes/07-create-telegram_user_info-table.yaml + - include: + file: db/changelog/changes/08-create-payments-table.yaml + - include: + file: db/changelog/changes/09-add-manager-and-2_customers-into-users-table.yaml + - include: + file: db/changelog/changes/10-add-2_cars-into-cars-table.yaml