From b22159dde34ff47b3a1a4c2453859d34f87fe177 Mon Sep 17 00:00:00 2001 From: mgucma Date: Thu, 3 Oct 2024 09:06:34 +0200 Subject: [PATCH 1/5] app --- .env | 8 + .github/workflows/codeql-analysis.yml | 64 ++++ Dockerfile | 17 + checkstyle.xml | 250 +++++++++++++++ compose.yaml | 35 +- pom.xml | 176 +++++++++-- .../carsharing/annotation/FieldMatch.java | 31 ++ .../annotation/FieldMatchValidator.java | 41 +++ .../marek/carsharing/config/MapperConfig.java | 14 + .../carsharing/config/SecurityConfig.java | 58 ++++ .../marek/carsharing/config/StripeConfig.java | 18 ++ .../controller/AuthenticationController.java | 43 +++ .../carsharing/controller/CarsController.java | 72 +++++ .../controller/PaymentsController.java | 63 ++++ .../controller/RentalsController.java | 67 ++++ .../controller/UsersController.java | 55 ++++ .../carsharing/dto/car/CarDetailsDto.java | 14 + .../com/marek/carsharing/dto/car/CarDto.java | 14 + .../carsharing/dto/car/CarRequestDto.java | 19 ++ .../dto/payment/CreatePaymentRequestDto.java | 9 + .../carsharing/dto/payment/PaymentDto.java | 13 + .../dto/payment/PaymentSearchParameters.java | 6 + .../dto/rental/CreateRentalRequestDto.java | 15 + .../carsharing/dto/rental/RentalDto.java | 14 + .../dto/rental/RentalSearchParameters.java | 8 + .../dto/user/UpdateUserRequestDto.java | 11 + .../marek/carsharing/dto/user/UserDto.java | 13 + .../dto/user/login/LoginRequestDto.java | 12 + .../dto/user/login/LoginResponseDto.java | 5 + .../user/registration/RegisterRequestDto.java | 28 ++ .../exception/BotNotificationException.java | 16 + .../CustomGlobalExceptionHandler.java | 53 ++++ .../exception/NoProviderException.java | 16 + .../exception/PaymentException.java | 16 + .../exception/RegistrationException.java | 16 + .../marek/carsharing/mapper/CarMapper.java | 21 ++ .../carsharing/mapper/PaymentMapper.java | 15 + .../marek/carsharing/mapper/RentalMapper.java | 15 + .../marek/carsharing/mapper/UserMapper.java | 23 ++ .../marek/carsharing/model/classes/Car.java | 48 +++ .../carsharing/model/classes/Payment.java | 53 ++++ .../carsharing/model/classes/Rental.java | 43 +++ .../marek/carsharing/model/classes/User.java | 91 ++++++ .../carsharing/model/enums/PaymentType.java | 7 + .../marek/carsharing/model/enums/Role.java | 6 + .../marek/carsharing/model/enums/Status.java | 8 + .../marek/carsharing/model/enums/Type.java | 5 + .../repository/SpecificationProvider.java | 13 + .../SpecificationProviderManager.java | 7 + .../repository/car/CarRepository.java | 13 + .../repository/payment/PaymentRepository.java | 14 + .../payment/SpecificationBuilder.java | 8 + .../provider/PaymentSpecificationBuilder.java | 33 ++ .../PaymentSpecificationProviderManager.java | 27 ++ .../UserIdSpecificationProvider.java | 37 +++ .../repository/rental/RentalRepository.java | 17 + .../rental/SpecificationBuilder.java | 9 + .../provider/RentalSpecificationBuilder.java | 40 +++ .../RentalSpecificationProviderManager.java | 27 ++ .../IsRentedSpecificationProvider.java | 58 ++++ .../UserIdRentalSpecificationProvider.java | 34 ++ .../repository/user/UserRepository.java | 17 + .../security/CustomUserDetailsService.java | 24 ++ .../carsharing/security/jwt/AuthService.java | 27 ++ .../security/jwt/JwtAuthFilter.java | 65 ++++ .../carsharing/security/jwt/JwtUtil.java | 55 ++++ .../carsharing/service/car/CarService.java | 20 ++ .../service/car/CarServiceImpl.java | 77 +++++ .../notification/NotificationService.java | 35 ++ .../service/payment/PaymentService.java | 18 ++ .../service/payment/PaymentServiceImpl.java | 179 +++++++++++ .../service/rental/RentalService.java | 18 ++ .../service/rental/RentalServiceImpl.java | 112 +++++++ .../carsharing/service/user/UserService.java | 16 + .../service/user/UserServiceImpl.java | 74 +++++ .../carsharing/telegram/NotificationBot.java | 44 +++ src/main/resources/application.properties | 19 ++ .../db/changelog/changes/01-create-user.yaml | 23 ++ .../db/changelog/changes/02-create-user.yaml | 23 ++ .../changelog/changes/create-car-table.yaml | 45 +++ .../changes/create-payment-table.yaml | 50 +++ .../changes/create-rental-table.yaml | 43 +++ .../changelog/changes/create-user-table.yaml | 46 +++ .../db/changelog/db.changelog-master.yaml | 13 + .../config/CustomPostgresContainer.java | 32 ++ .../controller/CarsControllerTest.java | 299 ++++++++++++++++++ .../controller/PaymentsControllerTest.java | 209 ++++++++++++ .../controller/RentalsControllerTest.java | 264 ++++++++++++++++ .../controller/UsersControllerTest.java | 216 +++++++++++++ .../rental/RentalRepositoryTest.java | 59 ++++ .../repository/user/UserRepositoryTest.java | 58 ++++ .../service/car/CarServiceImplTest.java | 242 ++++++++++++++ .../service/rental/RentalServiceImplTest.java | 230 ++++++++++++++ .../service/user/UserServiceImplTest.java | 155 +++++++++ src/test/resources/application.properties | 15 + .../resources/db/controller/add-to-cars.sql | 2 + .../resources/db/controller/add-to-cars2.sql | 2 + .../db/controller/add-to-payments.sql | 2 + .../db/controller/add-to-rentals.sql | 2 + .../resources/db/controller/add-to-users.sql | 2 + .../db/controller/delete-from-cars.sql | 1 + .../db/controller/delete-from-cars2.sql | 1 + .../db/controller/delete-from-payments.sql | 1 + .../db/controller/delete-from-rentals.sql | 1 + .../db/controller/delete-from-users.sql | 1 + .../resources/db/repo/add-to-rental-repo.sql | 2 + .../resources/db/repo/add-to-user-repo.sql | 2 + .../resources/db/repo/clean-rental-repo.sql | 1 + .../resources/db/repo/clean-user-repo.sql | 1 + 109 files changed, 4797 insertions(+), 28 deletions(-) create mode 100644 .env create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 Dockerfile create mode 100644 checkstyle.xml create mode 100644 src/main/java/com/marek/carsharing/annotation/FieldMatch.java create mode 100644 src/main/java/com/marek/carsharing/annotation/FieldMatchValidator.java create mode 100644 src/main/java/com/marek/carsharing/config/MapperConfig.java create mode 100644 src/main/java/com/marek/carsharing/config/SecurityConfig.java create mode 100644 src/main/java/com/marek/carsharing/config/StripeConfig.java create mode 100644 src/main/java/com/marek/carsharing/controller/AuthenticationController.java create mode 100644 src/main/java/com/marek/carsharing/controller/CarsController.java create mode 100644 src/main/java/com/marek/carsharing/controller/PaymentsController.java create mode 100644 src/main/java/com/marek/carsharing/controller/RentalsController.java create mode 100644 src/main/java/com/marek/carsharing/controller/UsersController.java create mode 100644 src/main/java/com/marek/carsharing/dto/car/CarDetailsDto.java create mode 100644 src/main/java/com/marek/carsharing/dto/car/CarDto.java create mode 100644 src/main/java/com/marek/carsharing/dto/car/CarRequestDto.java create mode 100644 src/main/java/com/marek/carsharing/dto/payment/CreatePaymentRequestDto.java create mode 100644 src/main/java/com/marek/carsharing/dto/payment/PaymentDto.java create mode 100644 src/main/java/com/marek/carsharing/dto/payment/PaymentSearchParameters.java create mode 100644 src/main/java/com/marek/carsharing/dto/rental/CreateRentalRequestDto.java create mode 100644 src/main/java/com/marek/carsharing/dto/rental/RentalDto.java create mode 100644 src/main/java/com/marek/carsharing/dto/rental/RentalSearchParameters.java create mode 100644 src/main/java/com/marek/carsharing/dto/user/UpdateUserRequestDto.java create mode 100644 src/main/java/com/marek/carsharing/dto/user/UserDto.java create mode 100644 src/main/java/com/marek/carsharing/dto/user/login/LoginRequestDto.java create mode 100644 src/main/java/com/marek/carsharing/dto/user/login/LoginResponseDto.java create mode 100644 src/main/java/com/marek/carsharing/dto/user/registration/RegisterRequestDto.java create mode 100644 src/main/java/com/marek/carsharing/exception/BotNotificationException.java create mode 100644 src/main/java/com/marek/carsharing/exception/CustomGlobalExceptionHandler.java create mode 100644 src/main/java/com/marek/carsharing/exception/NoProviderException.java create mode 100644 src/main/java/com/marek/carsharing/exception/PaymentException.java create mode 100644 src/main/java/com/marek/carsharing/exception/RegistrationException.java create mode 100644 src/main/java/com/marek/carsharing/mapper/CarMapper.java create mode 100644 src/main/java/com/marek/carsharing/mapper/PaymentMapper.java create mode 100644 src/main/java/com/marek/carsharing/mapper/RentalMapper.java create mode 100644 src/main/java/com/marek/carsharing/mapper/UserMapper.java create mode 100644 src/main/java/com/marek/carsharing/model/classes/Car.java create mode 100644 src/main/java/com/marek/carsharing/model/classes/Payment.java create mode 100644 src/main/java/com/marek/carsharing/model/classes/Rental.java create mode 100644 src/main/java/com/marek/carsharing/model/classes/User.java create mode 100644 src/main/java/com/marek/carsharing/model/enums/PaymentType.java create mode 100644 src/main/java/com/marek/carsharing/model/enums/Role.java create mode 100644 src/main/java/com/marek/carsharing/model/enums/Status.java create mode 100644 src/main/java/com/marek/carsharing/model/enums/Type.java create mode 100644 src/main/java/com/marek/carsharing/repository/SpecificationProvider.java create mode 100644 src/main/java/com/marek/carsharing/repository/SpecificationProviderManager.java create mode 100644 src/main/java/com/marek/carsharing/repository/car/CarRepository.java create mode 100644 src/main/java/com/marek/carsharing/repository/payment/PaymentRepository.java create mode 100644 src/main/java/com/marek/carsharing/repository/payment/SpecificationBuilder.java create mode 100644 src/main/java/com/marek/carsharing/repository/payment/provider/PaymentSpecificationBuilder.java create mode 100644 src/main/java/com/marek/carsharing/repository/payment/provider/PaymentSpecificationProviderManager.java create mode 100644 src/main/java/com/marek/carsharing/repository/payment/provider/specification/UserIdSpecificationProvider.java create mode 100644 src/main/java/com/marek/carsharing/repository/rental/RentalRepository.java create mode 100644 src/main/java/com/marek/carsharing/repository/rental/SpecificationBuilder.java create mode 100644 src/main/java/com/marek/carsharing/repository/rental/provider/RentalSpecificationBuilder.java create mode 100644 src/main/java/com/marek/carsharing/repository/rental/provider/RentalSpecificationProviderManager.java create mode 100644 src/main/java/com/marek/carsharing/repository/rental/provider/specification/IsRentedSpecificationProvider.java create mode 100644 src/main/java/com/marek/carsharing/repository/rental/provider/specification/UserIdRentalSpecificationProvider.java create mode 100644 src/main/java/com/marek/carsharing/repository/user/UserRepository.java create mode 100644 src/main/java/com/marek/carsharing/security/CustomUserDetailsService.java create mode 100644 src/main/java/com/marek/carsharing/security/jwt/AuthService.java create mode 100644 src/main/java/com/marek/carsharing/security/jwt/JwtAuthFilter.java create mode 100644 src/main/java/com/marek/carsharing/security/jwt/JwtUtil.java create mode 100644 src/main/java/com/marek/carsharing/service/car/CarService.java create mode 100644 src/main/java/com/marek/carsharing/service/car/CarServiceImpl.java create mode 100644 src/main/java/com/marek/carsharing/service/notification/NotificationService.java create mode 100644 src/main/java/com/marek/carsharing/service/payment/PaymentService.java create mode 100644 src/main/java/com/marek/carsharing/service/payment/PaymentServiceImpl.java create mode 100644 src/main/java/com/marek/carsharing/service/rental/RentalService.java create mode 100644 src/main/java/com/marek/carsharing/service/rental/RentalServiceImpl.java create mode 100644 src/main/java/com/marek/carsharing/service/user/UserService.java create mode 100644 src/main/java/com/marek/carsharing/service/user/UserServiceImpl.java create mode 100644 src/main/java/com/marek/carsharing/telegram/NotificationBot.java create mode 100644 src/main/resources/db/changelog/changes/01-create-user.yaml create mode 100644 src/main/resources/db/changelog/changes/02-create-user.yaml create mode 100644 src/main/resources/db/changelog/changes/create-car-table.yaml create mode 100644 src/main/resources/db/changelog/changes/create-payment-table.yaml create mode 100644 src/main/resources/db/changelog/changes/create-rental-table.yaml create mode 100644 src/main/resources/db/changelog/changes/create-user-table.yaml create mode 100644 src/main/resources/db/changelog/db.changelog-master.yaml create mode 100644 src/test/java/com/marek/carsharing/config/CustomPostgresContainer.java create mode 100644 src/test/java/com/marek/carsharing/controller/CarsControllerTest.java create mode 100644 src/test/java/com/marek/carsharing/controller/PaymentsControllerTest.java create mode 100644 src/test/java/com/marek/carsharing/controller/RentalsControllerTest.java create mode 100644 src/test/java/com/marek/carsharing/controller/UsersControllerTest.java create mode 100644 src/test/java/com/marek/carsharing/repository/rental/RentalRepositoryTest.java create mode 100644 src/test/java/com/marek/carsharing/repository/user/UserRepositoryTest.java create mode 100644 src/test/java/com/marek/carsharing/service/car/CarServiceImplTest.java create mode 100644 src/test/java/com/marek/carsharing/service/rental/RentalServiceImplTest.java create mode 100644 src/test/java/com/marek/carsharing/service/user/UserServiceImplTest.java create mode 100644 src/test/resources/application.properties create mode 100644 src/test/resources/db/controller/add-to-cars.sql create mode 100644 src/test/resources/db/controller/add-to-cars2.sql create mode 100644 src/test/resources/db/controller/add-to-payments.sql create mode 100644 src/test/resources/db/controller/add-to-rentals.sql create mode 100644 src/test/resources/db/controller/add-to-users.sql create mode 100644 src/test/resources/db/controller/delete-from-cars.sql create mode 100644 src/test/resources/db/controller/delete-from-cars2.sql create mode 100644 src/test/resources/db/controller/delete-from-payments.sql create mode 100644 src/test/resources/db/controller/delete-from-rentals.sql create mode 100644 src/test/resources/db/controller/delete-from-users.sql create mode 100644 src/test/resources/db/repo/add-to-rental-repo.sql create mode 100644 src/test/resources/db/repo/add-to-user-repo.sql create mode 100644 src/test/resources/db/repo/clean-rental-repo.sql create mode 100644 src/test/resources/db/repo/clean-user-repo.sql diff --git a/.env b/.env new file mode 100644 index 0000000..0397607 --- /dev/null +++ b/.env @@ -0,0 +1,8 @@ +POSTGRES_DATABASE=carservice +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres1234 +POSTGRES_DOCKER_PORT=5432 +POSTGRES_LOCAL_PORT=5433 +SPRING_DOCKER_PORT=8080 +SPRING_LOCAL_PORT=8081 +DEBUG_PORT=5005 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..e9776c0 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,64 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main ] + schedule: + - cron: '16 17 * * 4' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + permissions: + # required for all workflows + security-events: write + + # only required for workflows in private repositories + actions: read + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + # Override language selection by uncommenting this and choosing your languages + with: + languages: javascript + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below). + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # âœī¸ If the Autobuild fails above, remove it and uncomment the following + # three lines and modify them (or add more) to build your code if your + # project uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..305dd44 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# 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/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..4548f16 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,250 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/compose.yaml b/compose.yaml index 4d2047e..b7054b6 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,10 +1,31 @@ +version: "3.8" services: - mysql: - image: 'mysql:latest' + postgresdb: + image: postgres:15.2 + restart: unless-stopped + env_file: .env environment: - - 'MYSQL_DATABASE=mydatabase' - - 'MYSQL_PASSWORD=secret' - - 'MYSQL_ROOT_PASSWORD=verysecret' - - 'MYSQL_USER=myuser' + - POSTGRES_USER=$POSTGRES_USER + - POSTGRES_PASSWORD=$POSTGRES_PASSWORD + - POSTGRES_DB=$POSTGRES_DATABASE ports: - - '3306' + - ${POSTGRES_LOCAL_PORT}:${POSTGRES_DOCKER_PORT} + + app: + depends_on: + - postgresdb + image: book-store + restart: unless-stopped + build: . + env_file: .env + ports: + - ${SPRING_LOCAL_PORT}:${SPRING_DOCKER_PORT} + - ${DEBUG_PORT}:${DEBUG_PORT} + environment: + SPRING_APPLICATION_JSON: '{ + "spring.datasource.url" : "jdbc:postgresql://postgresdb:$POSTGRES_DOCKER_PORT/$POSTGRES_DATABASE", + "spring.datasource.username" : "$POSTGRES_USER", + "spring.datasource.password" : "$POSTGRES_PASSWORD", + "spring.jpa.hibernate.ddl-auto" : "validate" + }' + JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:${DEBUG_PORT}" \ No newline at end of file diff --git a/pom.xml b/pom.xml index 95b77f1..aa5f0ea 100644 --- a/pom.xml +++ b/pom.xml @@ -1,12 +1,13 @@ - 4.0.0 org.springframework.boot spring-boot-starter-parent 3.3.3 - + com.marek carsharing @@ -14,43 +15,41 @@ carsharing carsharing - - - - - - - - - - - - 17 + checkstyle.xml + 1.5.5.Final + 0.12.6 org.springframework.boot - spring-boot-starter-security + spring-boot-starter-data-jpa - org.springframework.boot - spring-boot-docker-compose - runtime - true + spring-boot-starter-web + + org.liquibase + liquibase-core + + + com.h2database h2 runtime + + - com.mysql - mysql-connector-j + org.postgresql + postgresql runtime + + org.projectlombok lombok @@ -61,9 +60,94 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + + + org.testcontainers + postgresql + test + + + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + + + org.springframework.boot + spring-boot-starter-validation + 3.3.2 + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.6.0 + + + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + + + + org.springframework.boot + spring-boot-starter-security + + + + + com.stripe + stripe-java + 22.0.0 + + + + javax.validation + validation-api + 2.0.1.Final + + + + + org.telegram + telegrambots + 6.9.0 + + + org.springframework.security spring-security-test + 6.3.0 test @@ -82,6 +166,56 @@ + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.0 + + + compile + + check + + + + + ${maven.checkstyle.plugin.configLocation} + true + true + false + src + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + + + org.projectlombok + lombok + 1.18.32 + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + -Xlint:deprecation + + + + diff --git a/src/main/java/com/marek/carsharing/annotation/FieldMatch.java b/src/main/java/com/marek/carsharing/annotation/FieldMatch.java new file mode 100644 index 0000000..b4099ab --- /dev/null +++ b/src/main/java/com/marek/carsharing/annotation/FieldMatch.java @@ -0,0 +1,31 @@ +package com.marek.carsharing.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.validation.Constraint; +import javax.validation.Payload; + +@Constraint(validatedBy = FieldMatchValidator.class) +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface FieldMatch { + + String message() default "Fields do not match"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + String first(); + + String second(); + + @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @interface List { + FieldMatch[] value(); + } +} + diff --git a/src/main/java/com/marek/carsharing/annotation/FieldMatchValidator.java b/src/main/java/com/marek/carsharing/annotation/FieldMatchValidator.java new file mode 100644 index 0000000..9c44886 --- /dev/null +++ b/src/main/java/com/marek/carsharing/annotation/FieldMatchValidator.java @@ -0,0 +1,41 @@ +package com.marek.carsharing.annotation; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import org.springframework.beans.BeanWrapperImpl; + +public class FieldMatchValidator implements ConstraintValidator { + + private String firstFieldName; + private String secondFieldName; + private String message; + + @Override + public void initialize(FieldMatch constraintAnnotation) { + this.firstFieldName = constraintAnnotation.first(); + this.secondFieldName = constraintAnnotation.second(); + this.message = constraintAnnotation.message(); + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + if (value == null) { + return true; + } + + BeanWrapperImpl wrapper = new BeanWrapperImpl(value); + Object firstField = wrapper.getPropertyValue(firstFieldName); + Object secondField = wrapper.getPropertyValue(secondFieldName); + + boolean valid = (firstField == null && secondField == null) + || (firstField != null && firstField.equals(secondField)); + + if (!valid) { + context.buildConstraintViolationWithTemplate(message) + .addPropertyNode(firstFieldName) + .addConstraintViolation() + .disableDefaultConstraintViolation(); + } + return valid; + } +} diff --git a/src/main/java/com/marek/carsharing/config/MapperConfig.java b/src/main/java/com/marek/carsharing/config/MapperConfig.java new file mode 100644 index 0000000..82a16a0 --- /dev/null +++ b/src/main/java/com/marek/carsharing/config/MapperConfig.java @@ -0,0 +1,14 @@ +package com.marek.carsharing.config; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.NullValueCheckStrategy; + +@org.mapstruct.MapperConfig( + componentModel = "spring", + injectionStrategy = InjectionStrategy.CONSTRUCTOR, + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, + implementationPackage = ".impl" +) +public class MapperConfig { +} + diff --git a/src/main/java/com/marek/carsharing/config/SecurityConfig.java b/src/main/java/com/marek/carsharing/config/SecurityConfig.java new file mode 100644 index 0000000..f710735 --- /dev/null +++ b/src/main/java/com/marek/carsharing/config/SecurityConfig.java @@ -0,0 +1,58 @@ +package com.marek.carsharing.config; + +import com.marek.carsharing.security.jwt.JwtAuthFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@EnableMethodSecurity +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + private final UserDetailsService userDetailsService; + private final JwtAuthFilter authFilter; + + @Bean + public PasswordEncoder getPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .cors(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests( + auth -> auth + .requestMatchers( + "/auth/**", + "/error", + "/swagger-ui/**") + .permitAll() + .anyRequest() + .authenticated() + ) + .httpBasic(Customizer.withDefaults()) + .addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class) + .userDetailsService(userDetailsService) + .build(); + } + + @Bean + public AuthenticationManager authenticationManager( + AuthenticationConfiguration authenticationConfiguration + ) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/marek/carsharing/config/StripeConfig.java b/src/main/java/com/marek/carsharing/config/StripeConfig.java new file mode 100644 index 0000000..409d72d --- /dev/null +++ b/src/main/java/com/marek/carsharing/config/StripeConfig.java @@ -0,0 +1,18 @@ +package com.marek.carsharing.config; + +import com.stripe.Stripe; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class StripeConfig { + @Value("${stripe.secret}") + private String stripeSecret; + + @PostConstruct + public void init() { + Stripe.apiKey = stripeSecret; + } +} + diff --git a/src/main/java/com/marek/carsharing/controller/AuthenticationController.java b/src/main/java/com/marek/carsharing/controller/AuthenticationController.java new file mode 100644 index 0000000..a346c8a --- /dev/null +++ b/src/main/java/com/marek/carsharing/controller/AuthenticationController.java @@ -0,0 +1,43 @@ +package com.marek.carsharing.controller; + +import com.marek.carsharing.dto.user.UserDto; +import com.marek.carsharing.dto.user.login.LoginRequestDto; +import com.marek.carsharing.dto.user.login.LoginResponseDto; +import com.marek.carsharing.dto.user.registration.RegisterRequestDto; +import com.marek.carsharing.security.jwt.AuthService; +import com.marek.carsharing.service.user.UserService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class AuthenticationController { + private final UserService userService; + private final AuthService authService; + + @Operation(summary = "Register a new user") + @PostMapping("/register") + @ResponseStatus(HttpStatus.CREATED) + public UserDto register( + @RequestBody @Valid RegisterRequestDto requestDto) { + return userService.register(requestDto); + } + + @Operation(summary = "User login to obtain JWT tokens") + @PostMapping("/login") + @ResponseStatus(HttpStatus.OK) + public LoginResponseDto login( + @RequestBody @Valid LoginRequestDto requestDto) { + return authService.authenticate(requestDto); + } +} + + diff --git a/src/main/java/com/marek/carsharing/controller/CarsController.java b/src/main/java/com/marek/carsharing/controller/CarsController.java new file mode 100644 index 0000000..103e82d --- /dev/null +++ b/src/main/java/com/marek/carsharing/controller/CarsController.java @@ -0,0 +1,72 @@ +package com.marek.carsharing.controller; + +import com.marek.carsharing.dto.car.CarDetailsDto; +import com.marek.carsharing.dto.car.CarDto; +import com.marek.carsharing.dto.car.CarRequestDto; +import com.marek.carsharing.service.car.CarService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/cars") +@RequiredArgsConstructor +public class CarsController { + private final CarService carService; + + @Operation(summary = "Add a new car - MANAGER only ", + description = "types = SEDAN, SUV, HATCHBACK, UNIVERSAL") + @PostMapping + @PreAuthorize("hasAnyAuthority('MANAGER')") + @ResponseStatus(HttpStatus.CREATED) + public CarDto addCar(@RequestBody @Valid CarRequestDto createCarRequestDto) { + return carService.addCar(createCarRequestDto); + } + + @Operation(summary = "Get a list of cars") + @GetMapping + @PreAuthorize("hasAnyAuthority('MANAGER', 'CUSTOMER')") + @ResponseStatus(HttpStatus.OK) + public List getCars(Pageable pageable) { + return carService.getCars(pageable); + } + + @Operation(summary = "Get car's detailed information") + @GetMapping("/{id}") + @PreAuthorize("hasAnyAuthority('MANAGER', 'CUSTOMER')") + @ResponseStatus(HttpStatus.OK) + public CarDetailsDto getCarDetails(@PathVariable Long id) { + return carService.getCarDetails(id); + } + + @Operation(summary = "Update car information - MANAGER only ", + description = "types = SEDAN, SUV, HATCHBACK, UNIVERSAL") + @PutMapping("/{id}") + @PreAuthorize("hasAnyAuthority('MANAGER')") + @ResponseStatus(HttpStatus.OK) + public CarDto updateCar(@PathVariable Long id, + @RequestBody @Valid CarRequestDto updateCarRequestDto) { + return carService.updateCar(id, updateCarRequestDto); + } + + @Operation(summary = "Delete car - MANAGER only ") + @DeleteMapping("/{id}") + @PreAuthorize("hasAnyAuthority('MANAGER', 'CUSTOMER')") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteCar(@PathVariable Long id) { + carService.delete(id); + } +} diff --git a/src/main/java/com/marek/carsharing/controller/PaymentsController.java b/src/main/java/com/marek/carsharing/controller/PaymentsController.java new file mode 100644 index 0000000..c5fb338 --- /dev/null +++ b/src/main/java/com/marek/carsharing/controller/PaymentsController.java @@ -0,0 +1,63 @@ +package com.marek.carsharing.controller; + +import com.marek.carsharing.dto.payment.CreatePaymentRequestDto; +import com.marek.carsharing.dto.payment.PaymentDto; +import com.marek.carsharing.dto.payment.PaymentSearchParameters; +import com.marek.carsharing.model.classes.User; +import com.marek.carsharing.service.payment.PaymentService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/payments") +@RequiredArgsConstructor +public class PaymentsController { + private final PaymentService paymentService; + + @Operation(summary = "Get payments by user ID - MANAGER only ") + @GetMapping + @PreAuthorize("hasAnyAuthority('MANAGER')") + @ResponseStatus(HttpStatus.OK) + public List getPayments(PaymentSearchParameters searchParameters) { + return paymentService.getPayments(searchParameters); + } + + @Operation(summary = "Create payment session") + @PostMapping + @PreAuthorize("hasAnyAuthority('MANAGER', 'CUSTOMER')") + @ResponseStatus(HttpStatus.CREATED) + public PaymentDto createPaymentSession(@RequestBody @Valid CreatePaymentRequestDto requestDto, + Authentication authentication) { + User user = (User) authentication.getPrincipal(); + return paymentService.createPayment(user, requestDto); + } + + @Operation(summary = "Check successful Stripe payments") + @GetMapping("/success/{sessionId}") + @PreAuthorize("hasAnyAuthority('MANAGER', 'CUSTOMER')") + @ResponseStatus(HttpStatus.OK) + public String checkPaymentSuccess(@PathVariable String sessionId) { + return paymentService.checkPaymentSuccess(sessionId); + } + + @Operation(summary = "Return payment paused message") + @GetMapping("/cancel/{sessionId}") + @PreAuthorize("hasAnyAuthority('MANAGER', 'CUSTOMER')") + @ResponseStatus(HttpStatus.OK) + public String paymentPaused(@PathVariable String sessionId) { + return paymentService.pausePayment(sessionId); + } +} + diff --git a/src/main/java/com/marek/carsharing/controller/RentalsController.java b/src/main/java/com/marek/carsharing/controller/RentalsController.java new file mode 100644 index 0000000..b225a8f --- /dev/null +++ b/src/main/java/com/marek/carsharing/controller/RentalsController.java @@ -0,0 +1,67 @@ +package com.marek.carsharing.controller; + +import com.marek.carsharing.dto.rental.CreateRentalRequestDto; +import com.marek.carsharing.dto.rental.RentalDto; +import com.marek.carsharing.dto.rental.RentalSearchParameters; +import com.marek.carsharing.model.classes.User; +import com.marek.carsharing.service.rental.RentalService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/rentals") +@RequiredArgsConstructor +public class RentalsController { + private final RentalService rentalService; + + @Operation(summary = "Add a new rental") + @PostMapping + @PreAuthorize("hasAnyAuthority('MANAGER', 'CUSTOMER')") + @ResponseStatus(HttpStatus.CREATED) + public RentalDto addRental( + @RequestBody @Valid CreateRentalRequestDto createRentalRequestDto, + Authentication authentication) { + User user = (User) authentication.getPrincipal(); + return rentalService.addRental(user, createRentalRequestDto); + } + + @Operation(summary = "Get rentals by user ID and whether the rental is active - MANAGER only ") + @GetMapping + @PreAuthorize("hasAnyAuthority('MANAGER')") + @ResponseStatus(HttpStatus.OK) + public List getRentals(RentalSearchParameters parameters) { + return rentalService.getRentals(parameters); + } + + @Operation(summary = "Get specific rental - MANAGER only ") + @GetMapping("/{id}") + @PreAuthorize("hasAnyAuthority('MANAGER')") + @ResponseStatus(HttpStatus.OK) + public RentalDto getRental(@PathVariable Long id) { + return rentalService.getRental(id); + } + + @Operation(summary = "Return a rental") + @PostMapping("/{id}/return") + @PreAuthorize("hasAnyAuthority('MANAGER', 'CUSTOMER')") + @ResponseStatus(HttpStatus.OK) + public void returnRental(@PathVariable Long id, + Authentication authentication) { + User user = (User) authentication.getPrincipal(); + rentalService.returnRental(user, id); + } +} + + diff --git a/src/main/java/com/marek/carsharing/controller/UsersController.java b/src/main/java/com/marek/carsharing/controller/UsersController.java new file mode 100644 index 0000000..5d6fac4 --- /dev/null +++ b/src/main/java/com/marek/carsharing/controller/UsersController.java @@ -0,0 +1,55 @@ +package com.marek.carsharing.controller; + +import com.marek.carsharing.dto.user.UpdateUserRequestDto; +import com.marek.carsharing.dto.user.UserDto; +import com.marek.carsharing.model.classes.User; +import com.marek.carsharing.service.user.UserService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/users") +@RequiredArgsConstructor +public class UsersController { + private final UserService userService; + + @Operation(summary = "Update user role - MANAGER only ", + description = """ + If user have Role = Manager then Manager => Customer,\n + If user have Role = Customer then Customer => Manager, + """) + @PutMapping("/{id}/role") + @PreAuthorize("hasAnyAuthority('MANAGER')") + @ResponseStatus(HttpStatus.OK) + public UserDto updateUserRole(@PathVariable Long id) { + return userService.updateRole(id); + } + + @Operation(summary = "Get my profile info") + @GetMapping("/me") + @ResponseStatus(HttpStatus.OK) + public UserDto getMyProfile(Authentication authentication) { + User user = (User) authentication.getPrincipal(); + return userService.getMyProfile(user); + } + + @Operation(summary = "Update my profile info") + @PutMapping("/me") + @ResponseStatus(HttpStatus.OK) + public UserDto updateMyProfile(Authentication authentication, + @RequestBody @Valid UpdateUserRequestDto request) { + User user = (User) authentication.getPrincipal(); + return userService.updateMyProfile(user, request); + } +} diff --git a/src/main/java/com/marek/carsharing/dto/car/CarDetailsDto.java b/src/main/java/com/marek/carsharing/dto/car/CarDetailsDto.java new file mode 100644 index 0000000..8ea8c76 --- /dev/null +++ b/src/main/java/com/marek/carsharing/dto/car/CarDetailsDto.java @@ -0,0 +1,14 @@ +package com.marek.carsharing.dto.car; + +import java.math.BigDecimal; +import lombok.Data; + +@Data +public class CarDetailsDto { + private Long id; + private String model; + private String brand; + private String type; + private int inventory; + private BigDecimal dailyFee; +} diff --git a/src/main/java/com/marek/carsharing/dto/car/CarDto.java b/src/main/java/com/marek/carsharing/dto/car/CarDto.java new file mode 100644 index 0000000..18f4400 --- /dev/null +++ b/src/main/java/com/marek/carsharing/dto/car/CarDto.java @@ -0,0 +1,14 @@ +package com.marek.carsharing.dto.car; + +import java.math.BigDecimal; +import lombok.Data; + +@Data +public class CarDto { + private Long id; + private String model; + private String brand; + private int inventory; + private BigDecimal dailyFee; +} + diff --git a/src/main/java/com/marek/carsharing/dto/car/CarRequestDto.java b/src/main/java/com/marek/carsharing/dto/car/CarRequestDto.java new file mode 100644 index 0000000..5d5172c --- /dev/null +++ b/src/main/java/com/marek/carsharing/dto/car/CarRequestDto.java @@ -0,0 +1,19 @@ +package com.marek.carsharing.dto.car; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.PositiveOrZero; +import java.math.BigDecimal; + +public record CarRequestDto( + @NotBlank + String model, + @NotBlank + String brand, + @NotBlank + String type, + @PositiveOrZero + int inventory, + @PositiveOrZero + BigDecimal dailyFee +) { +} diff --git a/src/main/java/com/marek/carsharing/dto/payment/CreatePaymentRequestDto.java b/src/main/java/com/marek/carsharing/dto/payment/CreatePaymentRequestDto.java new file mode 100644 index 0000000..fafc458 --- /dev/null +++ b/src/main/java/com/marek/carsharing/dto/payment/CreatePaymentRequestDto.java @@ -0,0 +1,9 @@ +package com.marek.carsharing.dto.payment; + +import jakarta.validation.constraints.PositiveOrZero; + +public record CreatePaymentRequestDto( + @PositiveOrZero + Long rentalId +) { +} diff --git a/src/main/java/com/marek/carsharing/dto/payment/PaymentDto.java b/src/main/java/com/marek/carsharing/dto/payment/PaymentDto.java new file mode 100644 index 0000000..74b4404 --- /dev/null +++ b/src/main/java/com/marek/carsharing/dto/payment/PaymentDto.java @@ -0,0 +1,13 @@ +package com.marek.carsharing.dto.payment; + +import java.math.BigDecimal; +import lombok.Data; + +@Data +public class PaymentDto { + private Long id; + private Long rentalId; + private String sessionUrl; + private String sessionId; + private BigDecimal amountToPay; +} diff --git a/src/main/java/com/marek/carsharing/dto/payment/PaymentSearchParameters.java b/src/main/java/com/marek/carsharing/dto/payment/PaymentSearchParameters.java new file mode 100644 index 0000000..8d45aba --- /dev/null +++ b/src/main/java/com/marek/carsharing/dto/payment/PaymentSearchParameters.java @@ -0,0 +1,6 @@ +package com.marek.carsharing.dto.payment; + +public record PaymentSearchParameters( + String[] usersId +) { +} diff --git a/src/main/java/com/marek/carsharing/dto/rental/CreateRentalRequestDto.java b/src/main/java/com/marek/carsharing/dto/rental/CreateRentalRequestDto.java new file mode 100644 index 0000000..dc5de0f --- /dev/null +++ b/src/main/java/com/marek/carsharing/dto/rental/CreateRentalRequestDto.java @@ -0,0 +1,15 @@ +package com.marek.carsharing.dto.rental; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import java.time.LocalDate; + +public record CreateRentalRequestDto( + @NotNull + LocalDate rentalDate, + @NotNull + LocalDate returnDate, + @PositiveOrZero + Long carId +) { +} diff --git a/src/main/java/com/marek/carsharing/dto/rental/RentalDto.java b/src/main/java/com/marek/carsharing/dto/rental/RentalDto.java new file mode 100644 index 0000000..f843a38 --- /dev/null +++ b/src/main/java/com/marek/carsharing/dto/rental/RentalDto.java @@ -0,0 +1,14 @@ +package com.marek.carsharing.dto.rental; + +import java.time.LocalDate; +import lombok.Data; + +@Data +public class RentalDto { + private Long id; + private LocalDate rentalDate; + private LocalDate actualReturnDate; + private LocalDate returnDate; + private Long carId; +} + diff --git a/src/main/java/com/marek/carsharing/dto/rental/RentalSearchParameters.java b/src/main/java/com/marek/carsharing/dto/rental/RentalSearchParameters.java new file mode 100644 index 0000000..9c96786 --- /dev/null +++ b/src/main/java/com/marek/carsharing/dto/rental/RentalSearchParameters.java @@ -0,0 +1,8 @@ +package com.marek.carsharing.dto.rental; + +public record RentalSearchParameters( + String[] userId, + Boolean isActive +) { +} + diff --git a/src/main/java/com/marek/carsharing/dto/user/UpdateUserRequestDto.java b/src/main/java/com/marek/carsharing/dto/user/UpdateUserRequestDto.java new file mode 100644 index 0000000..8818dd6 --- /dev/null +++ b/src/main/java/com/marek/carsharing/dto/user/UpdateUserRequestDto.java @@ -0,0 +1,11 @@ +package com.marek.carsharing.dto.user; + +import jakarta.validation.constraints.NotBlank; + +public record UpdateUserRequestDto( + @NotBlank + String firstName, + @NotBlank + String lastName +) { +} diff --git a/src/main/java/com/marek/carsharing/dto/user/UserDto.java b/src/main/java/com/marek/carsharing/dto/user/UserDto.java new file mode 100644 index 0000000..8c07bbd --- /dev/null +++ b/src/main/java/com/marek/carsharing/dto/user/UserDto.java @@ -0,0 +1,13 @@ +package com.marek.carsharing.dto.user; + +import lombok.Data; + +@Data +public class UserDto { + private Long id; + private String email; + private String firstName; + private String lastName; + private String role; +} + diff --git a/src/main/java/com/marek/carsharing/dto/user/login/LoginRequestDto.java b/src/main/java/com/marek/carsharing/dto/user/login/LoginRequestDto.java new file mode 100644 index 0000000..201e7b9 --- /dev/null +++ b/src/main/java/com/marek/carsharing/dto/user/login/LoginRequestDto.java @@ -0,0 +1,12 @@ +package com.marek.carsharing.dto.user.login; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record LoginRequestDto( + @NotBlank + @Email + String email, + @NotBlank + String password) { +} diff --git a/src/main/java/com/marek/carsharing/dto/user/login/LoginResponseDto.java b/src/main/java/com/marek/carsharing/dto/user/login/LoginResponseDto.java new file mode 100644 index 0000000..a9c73e7 --- /dev/null +++ b/src/main/java/com/marek/carsharing/dto/user/login/LoginResponseDto.java @@ -0,0 +1,5 @@ +package com.marek.carsharing.dto.user.login; + +public record LoginResponseDto(String token) { +} + diff --git a/src/main/java/com/marek/carsharing/dto/user/registration/RegisterRequestDto.java b/src/main/java/com/marek/carsharing/dto/user/registration/RegisterRequestDto.java new file mode 100644 index 0000000..e20a7b6 --- /dev/null +++ b/src/main/java/com/marek/carsharing/dto/user/registration/RegisterRequestDto.java @@ -0,0 +1,28 @@ +package com.marek.carsharing.dto.user.registration; + +import com.marek.carsharing.annotation.FieldMatch; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +@Data +@FieldMatch(first = "password", second = "repeatPassword", + message = "The password fields must match") +public class RegisterRequestDto { + @NotBlank + @Email + private String email; + @NotBlank + @Length(min = 6, max = 20) + private String password; + @NotBlank + @Length(min = 6, max = 20) + private String repeatPassword; + @NotBlank + private String firstName; + @NotBlank + private String lastName; +} + + diff --git a/src/main/java/com/marek/carsharing/exception/BotNotificationException.java b/src/main/java/com/marek/carsharing/exception/BotNotificationException.java new file mode 100644 index 0000000..302e4de --- /dev/null +++ b/src/main/java/com/marek/carsharing/exception/BotNotificationException.java @@ -0,0 +1,16 @@ +package com.marek.carsharing.exception; + +public class BotNotificationException extends RuntimeException { + public BotNotificationException(String message) { + super(message); + } + + public BotNotificationException(String message, Throwable cause) { + super(message, cause); + } + + public BotNotificationException(Throwable cause) { + super(cause); + } +} + diff --git a/src/main/java/com/marek/carsharing/exception/CustomGlobalExceptionHandler.java b/src/main/java/com/marek/carsharing/exception/CustomGlobalExceptionHandler.java new file mode 100644 index 0000000..4627ab0 --- /dev/null +++ b/src/main/java/com/marek/carsharing/exception/CustomGlobalExceptionHandler.java @@ -0,0 +1,53 @@ +package com.marek.carsharing.exception; + +import jakarta.persistence.EntityNotFoundException; +import java.time.LocalDateTime; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice +public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler { + @Override + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request + ) { + Map error = new LinkedHashMap<>(); + error.put("timestamp", LocalDateTime.now()); + error.put("status", HttpStatus.BAD_REQUEST); + List errorsList = ex.getBindingResult().getAllErrors().stream() + .map(this::getErrorMessage) + .toList(); + error.put("errors", errorsList); + return new ResponseEntity<>(error, headers, status); + } + + private String getErrorMessage(ObjectError objectError) { + if (objectError instanceof FieldError fieldError) { + String fieldName = fieldError.getField(); + String errorMessage = fieldError.getDefaultMessage(); + return String.format("%s: %s", fieldName, errorMessage); + } + return objectError.getDefaultMessage(); + } + + @ExceptionHandler(EntityNotFoundException.class) + public ResponseEntity handleEntityNotFoundException(EntityNotFoundException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage()); + } +} + diff --git a/src/main/java/com/marek/carsharing/exception/NoProviderException.java b/src/main/java/com/marek/carsharing/exception/NoProviderException.java new file mode 100644 index 0000000..f38ed5d --- /dev/null +++ b/src/main/java/com/marek/carsharing/exception/NoProviderException.java @@ -0,0 +1,16 @@ +package com.marek.carsharing.exception; + +public class NoProviderException extends RuntimeException { + public NoProviderException(String message) { + super(message); + } + + public NoProviderException(String message, Throwable cause) { + super(message, cause); + } + + public NoProviderException(Throwable cause) { + super(cause); + } +} + diff --git a/src/main/java/com/marek/carsharing/exception/PaymentException.java b/src/main/java/com/marek/carsharing/exception/PaymentException.java new file mode 100644 index 0000000..c1466a9 --- /dev/null +++ b/src/main/java/com/marek/carsharing/exception/PaymentException.java @@ -0,0 +1,16 @@ +package com.marek.carsharing.exception; + +public class PaymentException extends RuntimeException { + public PaymentException(String message) { + super(message); + } + + public PaymentException(String message, Throwable cause) { + super(message, cause); + } + + public PaymentException(Throwable cause) { + super(cause); + } +} + diff --git a/src/main/java/com/marek/carsharing/exception/RegistrationException.java b/src/main/java/com/marek/carsharing/exception/RegistrationException.java new file mode 100644 index 0000000..cd2bf20 --- /dev/null +++ b/src/main/java/com/marek/carsharing/exception/RegistrationException.java @@ -0,0 +1,16 @@ +package com.marek.carsharing.exception; + +public class RegistrationException extends RuntimeException { + public RegistrationException(String message) { + super(message); + } + + public RegistrationException(String message, Throwable cause) { + super(message, cause); + } + + public RegistrationException(Throwable cause) { + super(cause); + } +} + diff --git a/src/main/java/com/marek/carsharing/mapper/CarMapper.java b/src/main/java/com/marek/carsharing/mapper/CarMapper.java new file mode 100644 index 0000000..dca66c3 --- /dev/null +++ b/src/main/java/com/marek/carsharing/mapper/CarMapper.java @@ -0,0 +1,21 @@ +package com.marek.carsharing.mapper; + +import com.marek.carsharing.config.MapperConfig; +import com.marek.carsharing.dto.car.CarDetailsDto; +import com.marek.carsharing.dto.car.CarDto; +import com.marek.carsharing.dto.car.CarRequestDto; +import com.marek.carsharing.model.classes.Car; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; + +@Mapper(config = MapperConfig.class) +public interface CarMapper { + + CarDto toDto(Car car); + + CarDetailsDto toDetailsDto(Car car); + + Car toEntity(CarRequestDto carDto); + + void updateDto(CarRequestDto carDto, @MappingTarget Car car); +} diff --git a/src/main/java/com/marek/carsharing/mapper/PaymentMapper.java b/src/main/java/com/marek/carsharing/mapper/PaymentMapper.java new file mode 100644 index 0000000..a6bee2e --- /dev/null +++ b/src/main/java/com/marek/carsharing/mapper/PaymentMapper.java @@ -0,0 +1,15 @@ +package com.marek.carsharing.mapper; + +import com.marek.carsharing.config.MapperConfig; +import com.marek.carsharing.dto.payment.CreatePaymentRequestDto; +import com.marek.carsharing.dto.payment.PaymentDto; +import com.marek.carsharing.model.classes.Payment; +import org.mapstruct.Mapper; + +@Mapper(config = MapperConfig.class) +public interface PaymentMapper { + + PaymentDto toDto(Payment payment); + + Payment toEntity(CreatePaymentRequestDto paymentDto); +} diff --git a/src/main/java/com/marek/carsharing/mapper/RentalMapper.java b/src/main/java/com/marek/carsharing/mapper/RentalMapper.java new file mode 100644 index 0000000..3afce25 --- /dev/null +++ b/src/main/java/com/marek/carsharing/mapper/RentalMapper.java @@ -0,0 +1,15 @@ +package com.marek.carsharing.mapper; + +import com.marek.carsharing.config.MapperConfig; +import com.marek.carsharing.dto.rental.CreateRentalRequestDto; +import com.marek.carsharing.dto.rental.RentalDto; +import com.marek.carsharing.model.classes.Rental; +import org.mapstruct.Mapper; + +@Mapper(config = MapperConfig.class) +public interface RentalMapper { + + RentalDto toDto(Rental rental); + + Rental toEntity(CreateRentalRequestDto rentalDto); +} diff --git a/src/main/java/com/marek/carsharing/mapper/UserMapper.java b/src/main/java/com/marek/carsharing/mapper/UserMapper.java new file mode 100644 index 0000000..e91326c --- /dev/null +++ b/src/main/java/com/marek/carsharing/mapper/UserMapper.java @@ -0,0 +1,23 @@ +package com.marek.carsharing.mapper; + +import com.marek.carsharing.config.MapperConfig; +import com.marek.carsharing.dto.user.UpdateUserRequestDto; +import com.marek.carsharing.dto.user.UserDto; +import com.marek.carsharing.dto.user.registration.RegisterRequestDto; +import com.marek.carsharing.model.classes.User; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(config = MapperConfig.class) +public interface UserMapper { + + UserDto toDto(User user); + + User toEntity(UpdateUserRequestDto userDto); + + @Mapping(target = "email", source = "email") + @Mapping(target = "firstName", source = "firstName") + @Mapping(target = "lastName", source = "lastName") + User toEntity(RegisterRequestDto requestDto); +} + diff --git a/src/main/java/com/marek/carsharing/model/classes/Car.java b/src/main/java/com/marek/carsharing/model/classes/Car.java new file mode 100644 index 0000000..f21088c --- /dev/null +++ b/src/main/java/com/marek/carsharing/model/classes/Car.java @@ -0,0 +1,48 @@ +package com.marek.carsharing.model.classes; + +import com.marek.carsharing.model.enums.Type; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.SQLDelete; + +@Entity +@Table(name = "cars") +@Getter +@Setter +@NoArgsConstructor +@SQLDelete(sql = "UPDATE cars SET deleted = true WHERE id = ?") +public class Car { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String model; + + @Column(nullable = false) + private String brand; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Type type; + + @Column(nullable = false) + private int inventory; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal dailyFee; + + @Column(name = "deleted", nullable = false) + private boolean isDeleted = false; +} diff --git a/src/main/java/com/marek/carsharing/model/classes/Payment.java b/src/main/java/com/marek/carsharing/model/classes/Payment.java new file mode 100644 index 0000000..2d8befb --- /dev/null +++ b/src/main/java/com/marek/carsharing/model/classes/Payment.java @@ -0,0 +1,53 @@ +package com.marek.carsharing.model.classes; + +import com.marek.carsharing.model.enums.PaymentType; +import com.marek.carsharing.model.enums.Status; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.SQLDelete; + +@Entity +@Table(name = "payments") +@Getter +@Setter +@NoArgsConstructor +@SQLDelete(sql = "UPDATE payments SET deleted = true WHERE id = ?") +public class Payment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Status status; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PaymentType type; + + @Column(nullable = false) + private Long rentalId; + + @Column(nullable = false) + private String sessionUrl; + + @Column(nullable = false) + private String sessionId; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal amountToPay; + + @Column(name = "deleted", nullable = false) + private boolean isDeleted = false; +} diff --git a/src/main/java/com/marek/carsharing/model/classes/Rental.java b/src/main/java/com/marek/carsharing/model/classes/Rental.java new file mode 100644 index 0000000..0c2bce1 --- /dev/null +++ b/src/main/java/com/marek/carsharing/model/classes/Rental.java @@ -0,0 +1,43 @@ +package com.marek.carsharing.model.classes; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDate; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.SQLDelete; + +@Entity +@Table(name = "rentals") +@Getter +@Setter +@NoArgsConstructor +@SQLDelete(sql = "UPDATE rentals SET deleted = true WHERE id = ?") +public class Rental { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private LocalDate rentalDate; + + @Column(nullable = false) + private LocalDate returnDate; + + private LocalDate actualReturnDate; + + @Column(nullable = false) + private Long carId; + + @Column(nullable = false) + private Long userId; + + @Column(name = "deleted", nullable = false) + private boolean isDeleted = false; +} diff --git a/src/main/java/com/marek/carsharing/model/classes/User.java b/src/main/java/com/marek/carsharing/model/classes/User.java new file mode 100644 index 0000000..881c349 --- /dev/null +++ b/src/main/java/com/marek/carsharing/model/classes/User.java @@ -0,0 +1,91 @@ +package com.marek.carsharing.model.classes; + +import com.marek.carsharing.model.enums.Role; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.Collection; +import java.util.List; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.SQLDelete; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +@Entity +@Table(name = "users") +@Getter +@Setter +@NoArgsConstructor +@SQLDelete(sql = "UPDATE users SET deleted = true WHERE id = ?") +public class User implements UserDetails { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String email; + + @Column(nullable = false) + private String firstName; + + @Column(nullable = false) + private String lastName; + + @Column(nullable = false) + private String password; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Role role; + + @Column(name = "deleted", nullable = false) + private boolean isDeleted = false; + + @Override + public Collection getAuthorities() { + if (role.equals(Role.CUSTOMER)) { + return List.of(new SimpleGrantedAuthority("CUSTOMER")); + } else { + return List.of(new SimpleGrantedAuthority("MANAGER")); + } + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return email; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/marek/carsharing/model/enums/PaymentType.java b/src/main/java/com/marek/carsharing/model/enums/PaymentType.java new file mode 100644 index 0000000..ddc177b --- /dev/null +++ b/src/main/java/com/marek/carsharing/model/enums/PaymentType.java @@ -0,0 +1,7 @@ +package com.marek.carsharing.model.enums; + +public enum PaymentType { + PAYMENT, + FINE +} + diff --git a/src/main/java/com/marek/carsharing/model/enums/Role.java b/src/main/java/com/marek/carsharing/model/enums/Role.java new file mode 100644 index 0000000..34ab70d --- /dev/null +++ b/src/main/java/com/marek/carsharing/model/enums/Role.java @@ -0,0 +1,6 @@ +package com.marek.carsharing.model.enums; + +public enum Role { + MANAGER, CUSTOMER +} + diff --git a/src/main/java/com/marek/carsharing/model/enums/Status.java b/src/main/java/com/marek/carsharing/model/enums/Status.java new file mode 100644 index 0000000..a949212 --- /dev/null +++ b/src/main/java/com/marek/carsharing/model/enums/Status.java @@ -0,0 +1,8 @@ +package com.marek.carsharing.model.enums; + +public enum Status { + PENDING, + PAUSED, + PAID +} + diff --git a/src/main/java/com/marek/carsharing/model/enums/Type.java b/src/main/java/com/marek/carsharing/model/enums/Type.java new file mode 100644 index 0000000..4698edd --- /dev/null +++ b/src/main/java/com/marek/carsharing/model/enums/Type.java @@ -0,0 +1,5 @@ +package com.marek.carsharing.model.enums; + +public enum Type { + SEDAN, SUV, HATCHBACK, UNIVERSAL +} diff --git a/src/main/java/com/marek/carsharing/repository/SpecificationProvider.java b/src/main/java/com/marek/carsharing/repository/SpecificationProvider.java new file mode 100644 index 0000000..9c10a3e --- /dev/null +++ b/src/main/java/com/marek/carsharing/repository/SpecificationProvider.java @@ -0,0 +1,13 @@ +package com.marek.carsharing.repository; + +import org.springframework.data.jpa.domain.Specification; + +public interface SpecificationProvider { + String getKey(); + + Specification getSpecification(String[] params); + + Specification getSpecification(Long[] params); + + Specification getSpecification(Boolean params); +} diff --git a/src/main/java/com/marek/carsharing/repository/SpecificationProviderManager.java b/src/main/java/com/marek/carsharing/repository/SpecificationProviderManager.java new file mode 100644 index 0000000..99f7cf7 --- /dev/null +++ b/src/main/java/com/marek/carsharing/repository/SpecificationProviderManager.java @@ -0,0 +1,7 @@ +package com.marek.carsharing.repository; + +public interface SpecificationProviderManager { + + SpecificationProvider getSpecificationProvider(String key); +} + diff --git a/src/main/java/com/marek/carsharing/repository/car/CarRepository.java b/src/main/java/com/marek/carsharing/repository/car/CarRepository.java new file mode 100644 index 0000000..24d24df --- /dev/null +++ b/src/main/java/com/marek/carsharing/repository/car/CarRepository.java @@ -0,0 +1,13 @@ +package com.marek.carsharing.repository.car; + +import com.marek.carsharing.model.classes.Car; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +@Repository +public interface CarRepository extends JpaRepository, + JpaSpecificationExecutor { + List findAllByIsDeletedFalse(); +} diff --git a/src/main/java/com/marek/carsharing/repository/payment/PaymentRepository.java b/src/main/java/com/marek/carsharing/repository/payment/PaymentRepository.java new file mode 100644 index 0000000..cbab738 --- /dev/null +++ b/src/main/java/com/marek/carsharing/repository/payment/PaymentRepository.java @@ -0,0 +1,14 @@ +package com.marek.carsharing.repository.payment; + +import com.marek.carsharing.model.classes.Payment; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +@Repository +public interface PaymentRepository extends JpaRepository, + JpaSpecificationExecutor { + + Optional findBySessionIdAndIsDeletedFalse(String sessionId); +} diff --git a/src/main/java/com/marek/carsharing/repository/payment/SpecificationBuilder.java b/src/main/java/com/marek/carsharing/repository/payment/SpecificationBuilder.java new file mode 100644 index 0000000..d3a73e3 --- /dev/null +++ b/src/main/java/com/marek/carsharing/repository/payment/SpecificationBuilder.java @@ -0,0 +1,8 @@ +package com.marek.carsharing.repository.payment; + +import com.marek.carsharing.dto.payment.PaymentSearchParameters; +import org.springframework.data.jpa.domain.Specification; + +public interface SpecificationBuilder { + Specification build(PaymentSearchParameters searchParameters); +} diff --git a/src/main/java/com/marek/carsharing/repository/payment/provider/PaymentSpecificationBuilder.java b/src/main/java/com/marek/carsharing/repository/payment/provider/PaymentSpecificationBuilder.java new file mode 100644 index 0000000..30e8ead --- /dev/null +++ b/src/main/java/com/marek/carsharing/repository/payment/provider/PaymentSpecificationBuilder.java @@ -0,0 +1,33 @@ +package com.marek.carsharing.repository.payment.provider; + +import com.marek.carsharing.dto.payment.PaymentSearchParameters; +import com.marek.carsharing.model.classes.Payment; +import com.marek.carsharing.repository.SpecificationProviderManager; +import com.marek.carsharing.repository.payment.SpecificationBuilder; +import java.util.Arrays; +import lombok.RequiredArgsConstructor; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class PaymentSpecificationBuilder implements SpecificationBuilder { + private final SpecificationProviderManager specificationProviderManager; + + @Override + public Specification build(PaymentSearchParameters searchParameters) { + Specification spec = Specification.where(null); + if (searchParameters.usersId() != null + && searchParameters.usersId().length > 0) { + Long[] usersIds = Arrays.stream(searchParameters.usersId()) + .map(s -> s.replaceAll("[\\[\\]\"]", "")) + .map(Long::valueOf) + .toArray(Long[]::new); + spec = spec.and( + specificationProviderManager.getSpecificationProvider("usersId") + .getSpecification(usersIds)); + } + return spec; + } +} + diff --git a/src/main/java/com/marek/carsharing/repository/payment/provider/PaymentSpecificationProviderManager.java b/src/main/java/com/marek/carsharing/repository/payment/provider/PaymentSpecificationProviderManager.java new file mode 100644 index 0000000..fa99439 --- /dev/null +++ b/src/main/java/com/marek/carsharing/repository/payment/provider/PaymentSpecificationProviderManager.java @@ -0,0 +1,27 @@ +package com.marek.carsharing.repository.payment.provider; + +import com.marek.carsharing.exception.NoProviderException; +import com.marek.carsharing.model.classes.Payment; +import com.marek.carsharing.repository.SpecificationProvider; +import com.marek.carsharing.repository.SpecificationProviderManager; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class PaymentSpecificationProviderManager implements SpecificationProviderManager { + private final List> specificationProviders; + + @Override + public SpecificationProvider getSpecificationProvider(String key) { + return specificationProviders.stream() + .filter(p -> p.getKey().equals(key)) + .findFirst() + .orElseThrow( + () -> new NoProviderException( + "No payment specification provider found for key: " + key + ) + ); + } +} diff --git a/src/main/java/com/marek/carsharing/repository/payment/provider/specification/UserIdSpecificationProvider.java b/src/main/java/com/marek/carsharing/repository/payment/provider/specification/UserIdSpecificationProvider.java new file mode 100644 index 0000000..06b29af --- /dev/null +++ b/src/main/java/com/marek/carsharing/repository/payment/provider/specification/UserIdSpecificationProvider.java @@ -0,0 +1,37 @@ +package com.marek.carsharing.repository.payment.provider.specification; + +import com.marek.carsharing.model.classes.Payment; +import com.marek.carsharing.model.classes.Rental; +import com.marek.carsharing.repository.SpecificationProvider; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.JoinType; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; + +@Component +public class UserIdSpecificationProvider implements SpecificationProvider { + + public static final String USERS_ID = "usersId"; + + @Override + public String getKey() { + return USERS_ID; + } + + @Override + public Specification getSpecification(String[] params) { + return null; + } + + public Specification getSpecification(Long[] params) { + return (root, query, criteriaBuilder) -> { + Join rentalJoin = root.join("rental", JoinType.INNER); + return rentalJoin.get(USERS_ID).in((Object[]) params); + }; + } + + @Override + public Specification getSpecification(Boolean params) { + return null; + } +} diff --git a/src/main/java/com/marek/carsharing/repository/rental/RentalRepository.java b/src/main/java/com/marek/carsharing/repository/rental/RentalRepository.java new file mode 100644 index 0000000..f389870 --- /dev/null +++ b/src/main/java/com/marek/carsharing/repository/rental/RentalRepository.java @@ -0,0 +1,17 @@ +package com.marek.carsharing.repository.rental; + +import com.marek.carsharing.model.classes.Rental; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface RentalRepository extends JpaRepository, + JpaSpecificationExecutor { + + @Query("SELECT r FROM Rental r " + + "WHERE r.userId = :userId AND r.isDeleted = false") + Optional findByUserId(Long userId); +} diff --git a/src/main/java/com/marek/carsharing/repository/rental/SpecificationBuilder.java b/src/main/java/com/marek/carsharing/repository/rental/SpecificationBuilder.java new file mode 100644 index 0000000..594cbba --- /dev/null +++ b/src/main/java/com/marek/carsharing/repository/rental/SpecificationBuilder.java @@ -0,0 +1,9 @@ +package com.marek.carsharing.repository.rental; + +import com.marek.carsharing.dto.rental.RentalSearchParameters; +import org.springframework.data.jpa.domain.Specification; + +public interface SpecificationBuilder { + Specification build(RentalSearchParameters searchParameters); +} + diff --git a/src/main/java/com/marek/carsharing/repository/rental/provider/RentalSpecificationBuilder.java b/src/main/java/com/marek/carsharing/repository/rental/provider/RentalSpecificationBuilder.java new file mode 100644 index 0000000..5a5fbf3 --- /dev/null +++ b/src/main/java/com/marek/carsharing/repository/rental/provider/RentalSpecificationBuilder.java @@ -0,0 +1,40 @@ +package com.marek.carsharing.repository.rental.provider; + +import com.marek.carsharing.dto.rental.RentalSearchParameters; +import com.marek.carsharing.model.classes.Rental; +import com.marek.carsharing.repository.SpecificationProviderManager; +import com.marek.carsharing.repository.rental.SpecificationBuilder; +import java.util.Arrays; +import lombok.RequiredArgsConstructor; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RentalSpecificationBuilder implements SpecificationBuilder { + + private final SpecificationProviderManager specificationProviderManager; + + @Override + public Specification build(RentalSearchParameters searchParameters) { + Specification spec = Specification.where(null); + if (searchParameters.userId() != null + && searchParameters.userId().length > 0) { + Long[] userIds = Arrays.stream(searchParameters.userId()) + .map(s -> s.replaceAll("[\\[\\]\"]", "")) + .map(Long::valueOf) + .toArray(Long[]::new); + spec = spec.and( + specificationProviderManager.getSpecificationProvider("userId") + .getSpecification(userIds)); + } + if (searchParameters.isActive() != null) { + spec = spec.and( + specificationProviderManager.getSpecificationProvider("isActive") + .getSpecification(searchParameters.isActive())); + } + return spec; + } +} + + diff --git a/src/main/java/com/marek/carsharing/repository/rental/provider/RentalSpecificationProviderManager.java b/src/main/java/com/marek/carsharing/repository/rental/provider/RentalSpecificationProviderManager.java new file mode 100644 index 0000000..be61a47 --- /dev/null +++ b/src/main/java/com/marek/carsharing/repository/rental/provider/RentalSpecificationProviderManager.java @@ -0,0 +1,27 @@ +package com.marek.carsharing.repository.rental.provider; + +import com.marek.carsharing.exception.NoProviderException; +import com.marek.carsharing.model.classes.Rental; +import com.marek.carsharing.repository.SpecificationProvider; +import com.marek.carsharing.repository.SpecificationProviderManager; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RentalSpecificationProviderManager implements SpecificationProviderManager { + private final List> specificationProviders; + + @Override + public SpecificationProvider getSpecificationProvider(String key) { + return specificationProviders.stream() + .filter(p -> p.getKey().equals(key)) + .findFirst() + .orElseThrow( + () -> new NoProviderException( + "No specification provider found for key: " + key) + ); + } +} + diff --git a/src/main/java/com/marek/carsharing/repository/rental/provider/specification/IsRentedSpecificationProvider.java b/src/main/java/com/marek/carsharing/repository/rental/provider/specification/IsRentedSpecificationProvider.java new file mode 100644 index 0000000..210c380 --- /dev/null +++ b/src/main/java/com/marek/carsharing/repository/rental/provider/specification/IsRentedSpecificationProvider.java @@ -0,0 +1,58 @@ +package com.marek.carsharing.repository.rental.provider.specification; + +import com.marek.carsharing.model.classes.Rental; +import com.marek.carsharing.repository.SpecificationProvider; +import java.time.LocalDate; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; + +@Component +public class IsRentedSpecificationProvider implements SpecificationProvider { + + public static final String ACTUAL_RETURN_DATE = "actualReturnDate"; + public static final String RETURN_DATE = "returnDate"; + + @Override + public String getKey() { + return "isActive"; + } + + @Override + public Specification getSpecification(String[] params) { + return null; + } + + @Override + public Specification getSpecification(Long[] params) { + return null; + } + + @Override + public Specification getSpecification(Boolean params) { + return (root, query, criteriaBuilder) -> { + if (params == null) { + return null; + } + if (params) { + return criteriaBuilder.and( + criteriaBuilder.isNull( + root.get(ACTUAL_RETURN_DATE) + ), + criteriaBuilder.greaterThanOrEqualTo( + root.get(RETURN_DATE), + LocalDate.now() + )); + } else { + return criteriaBuilder.or( + criteriaBuilder.isNotNull( + root.get(ACTUAL_RETURN_DATE) + ), + criteriaBuilder.lessThan( + root.get(RETURN_DATE), + LocalDate.now() + )); + } + }; + } +} + diff --git a/src/main/java/com/marek/carsharing/repository/rental/provider/specification/UserIdRentalSpecificationProvider.java b/src/main/java/com/marek/carsharing/repository/rental/provider/specification/UserIdRentalSpecificationProvider.java new file mode 100644 index 0000000..7bba868 --- /dev/null +++ b/src/main/java/com/marek/carsharing/repository/rental/provider/specification/UserIdRentalSpecificationProvider.java @@ -0,0 +1,34 @@ +package com.marek.carsharing.repository.rental.provider.specification; + +import com.marek.carsharing.model.classes.Rental; +import com.marek.carsharing.repository.SpecificationProvider; +import java.util.Arrays; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; + +@Component +public class UserIdRentalSpecificationProvider implements SpecificationProvider { + + public static final String USER_ID = "userId"; + + @Override + public String getKey() { + return USER_ID; + } + + @Override + public Specification getSpecification(String[] params) { + return null; + } + + public Specification getSpecification(Long[] params) { + return (root, query, criteriaBuilder) -> root + .get(USER_ID) + .in(Arrays.stream(params).toArray()); + } + + @Override + public Specification getSpecification(Boolean params) { + return null; + } +} diff --git a/src/main/java/com/marek/carsharing/repository/user/UserRepository.java b/src/main/java/com/marek/carsharing/repository/user/UserRepository.java new file mode 100644 index 0000000..08a0a99 --- /dev/null +++ b/src/main/java/com/marek/carsharing/repository/user/UserRepository.java @@ -0,0 +1,17 @@ +package com.marek.carsharing.repository.user; + +import com.marek.carsharing.model.classes.User; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository, + JpaSpecificationExecutor { + + @Query("SELECT u FROM User u " + + "WHERE u.email = :email AND u.isDeleted = false") + Optional findUserByEmail(String email); +} diff --git a/src/main/java/com/marek/carsharing/security/CustomUserDetailsService.java b/src/main/java/com/marek/carsharing/security/CustomUserDetailsService.java new file mode 100644 index 0000000..c6600fd --- /dev/null +++ b/src/main/java/com/marek/carsharing/security/CustomUserDetailsService.java @@ -0,0 +1,24 @@ +package com.marek.carsharing.security; + +import com.marek.carsharing.repository.user.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + return userRepository.findUserByEmail(email).orElseThrow( + () -> new UsernameNotFoundException( + "User not found with username: " + email + ) + ); + } +} + diff --git a/src/main/java/com/marek/carsharing/security/jwt/AuthService.java b/src/main/java/com/marek/carsharing/security/jwt/AuthService.java new file mode 100644 index 0000000..84b1d0a --- /dev/null +++ b/src/main/java/com/marek/carsharing/security/jwt/AuthService.java @@ -0,0 +1,27 @@ +package com.marek.carsharing.security.jwt; + +import com.marek.carsharing.dto.user.login.LoginRequestDto; +import com.marek.carsharing.dto.user.login.LoginResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthService { + private final JwtUtil jwtUtil; + private final AuthenticationManager authenticationManager; + + public LoginResponseDto authenticate(LoginRequestDto loginRequestDto) { + Authentication authenticate = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + loginRequestDto.email(), loginRequestDto.password()) + ); + String token = jwtUtil.generateToken(authenticate.getName()); + + return new LoginResponseDto(token); + } +} + diff --git a/src/main/java/com/marek/carsharing/security/jwt/JwtAuthFilter.java b/src/main/java/com/marek/carsharing/security/jwt/JwtAuthFilter.java new file mode 100644 index 0000000..f6dfdd4 --- /dev/null +++ b/src/main/java/com/marek/carsharing/security/jwt/JwtAuthFilter.java @@ -0,0 +1,65 @@ +package com.marek.carsharing.security.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + public static final String BEARER = "Bearer "; + public static final int INDEX = BEARER.length(); + private final JwtUtil jwtUtil; + private final UserDetailsService userDetailsService; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + String token = getToken(request); + tokenValidation(token); + filterChain.doFilter(request, response); + } + + private void tokenValidation(String token) { + if (token != null && jwtUtil.isValidToken(token)) { + String username = jwtUtil.getName(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + Authentication authentication = + new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities() + ); + SecurityContextHolder + .getContext() + .setAuthentication(authentication); + } + } + + private String getToken(HttpServletRequest request) { + String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + return extractToken(authorization); + + } + + private static String extractToken(String authorization) { + if (StringUtils.hasText(authorization) + && authorization.startsWith(BEARER)) { + return authorization.substring(INDEX); + } + return null; + } +} + diff --git a/src/main/java/com/marek/carsharing/security/jwt/JwtUtil.java b/src/main/java/com/marek/carsharing/security/jwt/JwtUtil.java new file mode 100644 index 0000000..b7430f7 --- /dev/null +++ b/src/main/java/com/marek/carsharing/security/jwt/JwtUtil.java @@ -0,0 +1,55 @@ +package com.marek.carsharing.security.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Date; +import java.util.function.Function; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class JwtUtil { + @Value("${jwt.expiration}") + private Long expiration; + private Key key; + + public JwtUtil(@Value("${jwt.secret}") String secret) { + this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + } + + public String generateToken(String subject) { + + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(key) + .compact(); + } + + public boolean isValidToken(String token) { + Jws claimsJws = Jwts.parser() + .setSigningKey(key) + .build() + .parseClaimsJws(token); + + return claimsJws.getBody().getExpiration().before(new Date()); + } + + public String getName(String token) { + return getClaim(token, Claims::getSubject); + } + + private T getClaim(String token, Function claimsTFunction) { + Claims body = Jwts.parser() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + return claimsTFunction.apply(body); + } +} diff --git a/src/main/java/com/marek/carsharing/service/car/CarService.java b/src/main/java/com/marek/carsharing/service/car/CarService.java new file mode 100644 index 0000000..3099c8f --- /dev/null +++ b/src/main/java/com/marek/carsharing/service/car/CarService.java @@ -0,0 +1,20 @@ +package com.marek.carsharing.service.car; + +import com.marek.carsharing.dto.car.CarDetailsDto; +import com.marek.carsharing.dto.car.CarDto; +import com.marek.carsharing.dto.car.CarRequestDto; +import java.util.List; +import org.springframework.data.domain.Pageable; + +public interface CarService { + CarDto addCar(CarRequestDto createCarRequestDto); + + List getCars(Pageable pageable); + + CarDetailsDto getCarDetails(Long id); + + CarDto updateCar(Long id, CarRequestDto updateCarRequestDto); + + void delete(Long id); +} + diff --git a/src/main/java/com/marek/carsharing/service/car/CarServiceImpl.java b/src/main/java/com/marek/carsharing/service/car/CarServiceImpl.java new file mode 100644 index 0000000..440a81b --- /dev/null +++ b/src/main/java/com/marek/carsharing/service/car/CarServiceImpl.java @@ -0,0 +1,77 @@ +package com.marek.carsharing.service.car; + +import com.marek.carsharing.dto.car.CarDetailsDto; +import com.marek.carsharing.dto.car.CarDto; +import com.marek.carsharing.dto.car.CarRequestDto; +import com.marek.carsharing.mapper.CarMapper; +import com.marek.carsharing.model.classes.Car; +import com.marek.carsharing.model.enums.Type; +import com.marek.carsharing.repository.car.CarRepository; +import jakarta.persistence.EntityNotFoundException; +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CarServiceImpl implements CarService { + private final CarRepository carRepository; + private final CarMapper carMapper; + + @Override + @Transactional + public CarDto addCar(CarRequestDto createCarRequestDto) { + Car car = carMapper.toEntity(createCarRequestDto); + setTypeForCar(createCarRequestDto, car); + return carMapper.toDto(carRepository.save(car)); + } + + @Override + public List getCars(Pageable pageable) { + return carRepository.findAll(pageable).stream() + .map(carMapper::toDto) + .toList(); + } + + @Override + public CarDetailsDto getCarDetails(Long id) { + return carMapper.toDetailsDto( + carRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException("Car not found with id: " + id) + )); + } + + @Override + @Transactional + public CarDto updateCar(Long id, @Valid CarRequestDto updateCarRequestDto) { + Car car = carRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException( + "Car not found with id: " + id + ) + ); + carMapper.updateDto(updateCarRequestDto, car); + + setTypeForCar(updateCarRequestDto, car); + + return carMapper.toDto(carRepository.save(car)); + + } + + @Override + public void delete(Long id) { + carRepository.deleteById(id); + } + + private static void setTypeForCar(CarRequestDto createCarRequestDto, Car car) { + switch (createCarRequestDto.type().toUpperCase()) { + case "SEDAN" -> car.setType(Type.SEDAN); + case "SUV" -> car.setType(Type.SUV); + case "HATCHBACK" -> car.setType(Type.HATCHBACK); + default -> car.setType(Type.UNIVERSAL); + } + } +} + diff --git a/src/main/java/com/marek/carsharing/service/notification/NotificationService.java b/src/main/java/com/marek/carsharing/service/notification/NotificationService.java new file mode 100644 index 0000000..cfb53f5 --- /dev/null +++ b/src/main/java/com/marek/carsharing/service/notification/NotificationService.java @@ -0,0 +1,35 @@ +package com.marek.carsharing.service.notification; + +import com.marek.carsharing.telegram.NotificationBot; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class NotificationService { + private final NotificationBot notificationBot; + @Value("${telegram.admin.chat.id}") + private String adminChatId; + + public void notifyNewRentalsCreated(String message) { + String info = """ + new rentals created: + """ + message; + notificationBot.sendMessage(adminChatId, info); + } + + public void notifyOverdueRentals(String message) { + String info = """ + overdue rentals: + """ + message; + notificationBot.sendMessage(adminChatId, info); + } + + public void notifySuccessfulPayments(String message) { + String info = """ + successful payments: + """ + message; + notificationBot.sendMessage(adminChatId, info); + } +} diff --git a/src/main/java/com/marek/carsharing/service/payment/PaymentService.java b/src/main/java/com/marek/carsharing/service/payment/PaymentService.java new file mode 100644 index 0000000..d20e7b5 --- /dev/null +++ b/src/main/java/com/marek/carsharing/service/payment/PaymentService.java @@ -0,0 +1,18 @@ +package com.marek.carsharing.service.payment; + +import com.marek.carsharing.dto.payment.CreatePaymentRequestDto; +import com.marek.carsharing.dto.payment.PaymentDto; +import com.marek.carsharing.dto.payment.PaymentSearchParameters; +import com.marek.carsharing.model.classes.User; +import java.util.List; + +public interface PaymentService { + PaymentDto createPayment(User user, CreatePaymentRequestDto requestDto); + + String checkPaymentSuccess(String sessionId); + + String pausePayment(String sessionId); + + List getPayments(PaymentSearchParameters searchParameters); +} + diff --git a/src/main/java/com/marek/carsharing/service/payment/PaymentServiceImpl.java b/src/main/java/com/marek/carsharing/service/payment/PaymentServiceImpl.java new file mode 100644 index 0000000..f17921a --- /dev/null +++ b/src/main/java/com/marek/carsharing/service/payment/PaymentServiceImpl.java @@ -0,0 +1,179 @@ +package com.marek.carsharing.service.payment; + +import com.marek.carsharing.dto.payment.CreatePaymentRequestDto; +import com.marek.carsharing.dto.payment.PaymentDto; +import com.marek.carsharing.dto.payment.PaymentSearchParameters; +import com.marek.carsharing.exception.PaymentException; +import com.marek.carsharing.mapper.PaymentMapper; +import com.marek.carsharing.model.classes.Car; +import com.marek.carsharing.model.classes.Payment; +import com.marek.carsharing.model.classes.Rental; +import com.marek.carsharing.model.classes.User; +import com.marek.carsharing.model.enums.PaymentType; +import com.marek.carsharing.model.enums.Status; +import com.marek.carsharing.repository.car.CarRepository; +import com.marek.carsharing.repository.payment.PaymentRepository; +import com.marek.carsharing.repository.payment.provider.PaymentSpecificationBuilder; +import com.marek.carsharing.repository.rental.RentalRepository; +import com.marek.carsharing.service.notification.NotificationService; +import com.stripe.exception.StripeException; +import com.stripe.model.checkout.Session; +import com.stripe.param.checkout.SessionCreateParams; +import jakarta.persistence.EntityNotFoundException; +import java.math.BigDecimal; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PaymentServiceImpl implements PaymentService { + private static final String paymentDomain = "/payments/"; + private static final String COMPLETE = "complete"; + private static final String OPEN = "open"; + private static final String USD = "usd"; + + @Value("${domain}") + private String domain; + + private final RentalRepository rentalRepository; + private final CarRepository carRepository; + private final PaymentRepository paymentRepository; + private final PaymentMapper paymentMapper; + private final NotificationService notificationService; + private final PaymentSpecificationBuilder paymentSpecificationBuilder; + + @Override + @Transactional + public PaymentDto createPayment(User user, CreatePaymentRequestDto requestDto) { + Rental rental = rentalRepository.findById(requestDto.rentalId()).orElseThrow( + () -> new EntityNotFoundException( + "Rental with id " + requestDto.rentalId() + " not found") + ); + BigDecimal total = getAmountToPay(rental); + + try { + Session session = createStripeSession(total, rental); + Payment payment = getPayment(Status.PENDING, + PaymentType.PAYMENT, + rental, + total, + session); + + return paymentMapper.toDto( + paymentRepository.save(payment)); + + } catch (StripeException e) { + throw new PaymentException("cannot create payment", e); + } + } + + private Session createStripeSession(BigDecimal total, Rental rental) throws StripeException { + SessionCreateParams rentInUsd = SessionCreateParams.builder() + .setMode(SessionCreateParams.Mode.PAYMENT) + .setSuccessUrl(domain + paymentDomain + "success/{CHECKOUT_SESSION_ID}") + .setCancelUrl(domain + paymentDomain + "cancel/{CHECKOUT_SESSION_ID}") + .addLineItem(SessionCreateParams.LineItem.builder() + .setPriceData(SessionCreateParams.LineItem.PriceData.builder() + .setCurrency(USD) + .setUnitAmount(total.multiply(BigDecimal.valueOf(100)).longValue()) + .setProductData( + SessionCreateParams.LineItem.PriceData.ProductData.builder() + .setName("Payment for: " + rental.getId()) + .build()) + .build()) + .setQuantity(1L) + .build()) + .setClientReferenceId(rental.getId().toString()) + .build(); + return Session.create(rentInUsd); + } + + private BigDecimal getAmountToPay(Rental rental) { + Car car = carRepository.findById(rental.getCarId()).orElseThrow( + () -> new EntityNotFoundException("Car with id " + rental.getCarId() + " not found") + ); + BigDecimal dailyFee = car.getDailyFee(); + long start = rental.getRentalDate().toEpochDay(); + long end = rental.getReturnDate().toEpochDay(); + long daysAmount = end - start; + return dailyFee.multiply(BigDecimal.valueOf(daysAmount)); + } + + private Payment getPayment(Status pending, + PaymentType payment, + Rental rental, + BigDecimal total, + Session session) { + Payment paymentEntity = new Payment(); + paymentEntity.setStatus(pending); + paymentEntity.setType(payment); + paymentEntity.setRentalId(rental.getId()); + paymentEntity.setSessionUrl(session.getUrl()); + paymentEntity.setSessionId(session.getId()); + paymentEntity.setAmountToPay(total); + paymentEntity.setDeleted(false); + return paymentEntity; + } + + @Override + public String checkPaymentSuccess(String sessionId) { + try { + Session session = Session.retrieve(sessionId); + if (COMPLETE.equals(session.getStatus())) { + Payment payment = paymentRepository.findBySessionIdAndIsDeletedFalse(sessionId) + .orElseThrow(() -> + new PaymentException("Cannot find payment with session id " + + sessionId)); + payment.setStatus(Status.PAID); + paymentRepository.save(payment); + String message = "Payment successful! Thank you for your payment."; + notificationService.notifySuccessfulPayments(message); + return message; + } else { + return "Payment is not completed yet. Please try again later. In Success"; + } + } catch (StripeException e) { + throw new PaymentException("cannot retrieve payment success", e); + } + } + + @Override + public String pausePayment(String sessionId) { + try { + Session session = Session.retrieve(sessionId); + if (OPEN.equals(session.getStatus())) { + Payment payment = paymentRepository.findBySessionIdAndIsDeletedFalse(sessionId) + .orElseThrow(() -> + new PaymentException("Cannot find payment with session id " + + sessionId)); + payment.setStatus(Status.PAUSED); + paymentRepository.save(payment); + return "Payment paused. You can resume your payment later."; + } else if (COMPLETE.equals(session.getStatus())) { + return "Payment has been paid. Thank you for your payment."; + } else { + return "Payment is not completed yet. Please try again later. In Pause"; + } + } catch (StripeException e) { + throw new PaymentException("cannot retrieve payment success", e); + } + } + + @Override + public List getPayments(PaymentSearchParameters searchParameters) { + Specification build = paymentSpecificationBuilder.build(searchParameters); + List all = paymentRepository.findAll(build); + + if (all.isEmpty()) { + throw new EntityNotFoundException("No payments found for the given search parameters"); + } + + return all.stream() + .map(paymentMapper::toDto) + .toList(); + } +} diff --git a/src/main/java/com/marek/carsharing/service/rental/RentalService.java b/src/main/java/com/marek/carsharing/service/rental/RentalService.java new file mode 100644 index 0000000..08d32e6 --- /dev/null +++ b/src/main/java/com/marek/carsharing/service/rental/RentalService.java @@ -0,0 +1,18 @@ +package com.marek.carsharing.service.rental; + +import com.marek.carsharing.dto.rental.CreateRentalRequestDto; +import com.marek.carsharing.dto.rental.RentalDto; +import com.marek.carsharing.dto.rental.RentalSearchParameters; +import com.marek.carsharing.model.classes.User; +import java.util.List; + +public interface RentalService { + RentalDto addRental(User user, CreateRentalRequestDto createRentalRequestDto); + + List getRentals(RentalSearchParameters rentalSearchParameters); + + RentalDto getRental(Long id); + + void returnRental(User user, Long id); +} + diff --git a/src/main/java/com/marek/carsharing/service/rental/RentalServiceImpl.java b/src/main/java/com/marek/carsharing/service/rental/RentalServiceImpl.java new file mode 100644 index 0000000..b7be8fb --- /dev/null +++ b/src/main/java/com/marek/carsharing/service/rental/RentalServiceImpl.java @@ -0,0 +1,112 @@ +package com.marek.carsharing.service.rental; + +import com.marek.carsharing.dto.rental.CreateRentalRequestDto; +import com.marek.carsharing.dto.rental.RentalDto; +import com.marek.carsharing.dto.rental.RentalSearchParameters; +import com.marek.carsharing.mapper.RentalMapper; +import com.marek.carsharing.model.classes.Car; +import com.marek.carsharing.model.classes.Rental; +import com.marek.carsharing.model.classes.User; +import com.marek.carsharing.repository.car.CarRepository; +import com.marek.carsharing.repository.rental.RentalRepository; +import com.marek.carsharing.repository.rental.provider.RentalSpecificationBuilder; +import com.marek.carsharing.service.notification.NotificationService; +import jakarta.persistence.EntityNotFoundException; +import java.time.LocalDate; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class RentalServiceImpl implements RentalService { + private final RentalRepository rentalRepository; + private final RentalMapper rentalMapper; + private final CarRepository carRepository; + private final RentalSpecificationBuilder rentalSpecificationBuilder; + private final NotificationService notificationService; + + @Override + @Transactional + public RentalDto addRental( + User user, CreateRentalRequestDto createRentalRequestDto) { + updateCarInventoryAfterRent(createRentalRequestDto); + notificationService.notifyNewRentalsCreated( + "User with id " + user.getId() + + " rent a car with id " + createRentalRequestDto.carId() + + " from " + createRentalRequestDto.rentalDate() + + " to " + createRentalRequestDto.returnDate() + ); + Rental entity = rentalMapper.toEntity(createRentalRequestDto); + entity.setUserId(user.getId()); + return rentalMapper.toDto( + rentalRepository.save(entity) + ); + } + + private void updateCarInventoryAfterRent(CreateRentalRequestDto createRentalRequestDto) { + Car car = carRepository.findById(createRentalRequestDto.carId()).orElseThrow( + () -> new EntityNotFoundException( + "Car with id " + createRentalRequestDto.carId() + " not found") + ); + car.setInventory(car.getInventory() - 1); + } + + @Override + public List getRentals(RentalSearchParameters rentalSearchParameters) { + Specification build = rentalSpecificationBuilder.build(rentalSearchParameters); + return rentalRepository.findAll(build).stream() + .map(rentalMapper::toDto) + .toList(); + } + + @Override + public RentalDto getRental(Long id) { + return rentalMapper.toDto( + rentalRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException("Rental with id " + id + " not found") + ) + ); + } + + @Override + @Transactional + public void returnRental(User user, Long id) { + Rental rental = rentalRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException("Rental with id " + id + " not found") + ); + checkIfThisRentIsForCorrectUser(user, rental); + + rental.setActualReturnDate(LocalDate.now()); + notificationForOverdueRent(rental); + + rental.setId(id); + Long carId = rental.getCarId(); + Car car = carRepository.findById(carId).orElseThrow( + () -> new EntityNotFoundException("Car with id " + carId + " not found") + ); + car.setInventory(car.getInventory() + 1); + + rentalRepository.save(rental); + } + + private void notificationForOverdueRent(Rental rental) { + if (rental.getReturnDate().isBefore(LocalDate.now())) { + notificationService.notifyOverdueRentals( + "User with id " + rental.getUserId() + + " returned the car late, car id " + rental.getCarId() + ); + } + } + + private static void checkIfThisRentIsForCorrectUser(User user, Rental rental) { + if (!user.getId().equals(rental.getUserId())) { + throw new EntityNotFoundException( + "User with id " + user.getId() + + " does not belong to this Rental" + ); + } + } +} diff --git a/src/main/java/com/marek/carsharing/service/user/UserService.java b/src/main/java/com/marek/carsharing/service/user/UserService.java new file mode 100644 index 0000000..39a4a3f --- /dev/null +++ b/src/main/java/com/marek/carsharing/service/user/UserService.java @@ -0,0 +1,16 @@ +package com.marek.carsharing.service.user; + +import com.marek.carsharing.dto.user.UpdateUserRequestDto; +import com.marek.carsharing.dto.user.UserDto; +import com.marek.carsharing.dto.user.registration.RegisterRequestDto; +import com.marek.carsharing.model.classes.User; + +public interface UserService { + UserDto register(RegisterRequestDto requestDto); + + UserDto updateRole(Long id); + + UserDto getMyProfile(User user); + + UserDto updateMyProfile(User user, UpdateUserRequestDto requestDto); +} diff --git a/src/main/java/com/marek/carsharing/service/user/UserServiceImpl.java b/src/main/java/com/marek/carsharing/service/user/UserServiceImpl.java new file mode 100644 index 0000000..bca9229 --- /dev/null +++ b/src/main/java/com/marek/carsharing/service/user/UserServiceImpl.java @@ -0,0 +1,74 @@ +package com.marek.carsharing.service.user; + +import com.marek.carsharing.dto.user.UpdateUserRequestDto; +import com.marek.carsharing.dto.user.UserDto; +import com.marek.carsharing.dto.user.registration.RegisterRequestDto; +import com.marek.carsharing.exception.RegistrationException; +import com.marek.carsharing.mapper.UserMapper; +import com.marek.carsharing.model.classes.User; +import com.marek.carsharing.model.enums.Role; +import com.marek.carsharing.repository.user.UserRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UserServiceImpl implements UserService { + public static final Role ROLE = Role.CUSTOMER; + private final UserRepository userRepository; + private final UserMapper userMapper; + private final PasswordEncoder passwordEncoder; + + @Override + public UserDto register(RegisterRequestDto requestDto) { + if (userRepository.findUserByEmail(requestDto.getEmail()).isPresent()) { + throw new RegistrationException("User with this email: " + + requestDto.getEmail() + " already exist"); + } + User user = registerNewUser(requestDto); + return userMapper.toDto( + userRepository.save(user)); + } + + @Transactional + protected User registerNewUser(RegisterRequestDto requestDto) { + User user = userMapper.toEntity(requestDto); + user.setPassword(passwordEncoder.encode(requestDto.getPassword())); + user.setRole(ROLE); + return user; + } + + @Override + @Transactional + public UserDto updateRole(Long id) { + User user = userRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException("user with this id does not exist") + ); + switch (user.getRole()) { + case CUSTOMER -> user.setRole(Role.MANAGER); + default -> user.setRole(Role.CUSTOMER); + } + return userMapper.toDto( + userRepository.save(user) + ); + } + + @Override + public UserDto getMyProfile(User user) { + return userMapper.toDto(user); + } + + @Override + @Transactional + public UserDto updateMyProfile(User user, UpdateUserRequestDto requestDto) { + user.setFirstName(requestDto.firstName()); + user.setLastName(requestDto.lastName()); + return userMapper.toDto( + userRepository.save(user) + ); + } +} + diff --git a/src/main/java/com/marek/carsharing/telegram/NotificationBot.java b/src/main/java/com/marek/carsharing/telegram/NotificationBot.java new file mode 100644 index 0000000..1b75b2d --- /dev/null +++ b/src/main/java/com/marek/carsharing/telegram/NotificationBot.java @@ -0,0 +1,44 @@ +package com.marek.carsharing.telegram; + +import com.marek.carsharing.exception.BotNotificationException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.telegram.telegrambots.bots.TelegramLongPollingBot; +import org.telegram.telegrambots.meta.api.methods.send.SendMessage; +import org.telegram.telegrambots.meta.api.objects.Update; +import org.telegram.telegrambots.meta.exceptions.TelegramApiException; + +@Component +public class NotificationBot extends TelegramLongPollingBot { + @Value("${telegram.bot.token}") + private String botToken; + @Value("${telegram.bot.username}") + private String botUsername; + + @Override + public void onUpdateReceived(Update update) { + } + + @Override + public String getBotUsername() { + return botUsername; + } + + @Override + public String getBotToken() { + return botToken; + } + + public void sendMessage(String chatId, String message) { + SendMessage sendMessage = new SendMessage(); + sendMessage.setChatId(chatId); + sendMessage.setText(message); + + try { + execute(sendMessage); + } catch (TelegramApiException e) { + throw new BotNotificationException("Cannot send message", e); + } + } +} + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 73fa737..cbfeca8 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,20 @@ spring.application.name=carsharing + +spring.datasource.url=jdbc:postgresql://localhost/car-service +spring.datasource.username=root +spring.datasource.password=root + +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.hibernate.ddl-auto=validate +spring.jpa.show-sql=true + +jwt.expiration=500000000 +jwt.secret=dfgdfhzvatggbchbfhgvxfgvfgvxcv74743828293 + +stripe.secret=sk_test_51Ppl3K04HmZD0PeB5oOkRtz\ + PxRmL9tMvWbc9ucS1Xoa1cf0REEYgnlFoR9Pw717jL5txghUOyLOp17DpaSM9G6ea00yYrGMmfT +domain=http://localhost:8080 + +telegram.bot.token=7409797315:AAGrN3mp85YrH2wvMH8XZ4EW0No6LibYMWs +telegram.bot.username=marekcarsharingBot +telegram.admin.chat.id=6469260399 diff --git a/src/main/resources/db/changelog/changes/01-create-user.yaml b/src/main/resources/db/changelog/changes/01-create-user.yaml new file mode 100644 index 0000000..0ac21bc --- /dev/null +++ b/src/main/resources/db/changelog/changes/01-create-user.yaml @@ -0,0 +1,23 @@ +databaseChangeLog: + - changeSet: + id: 01-create-user + author: mg + changes: + - insert: + tableName: users + columns: + - column: + name: email + value: admin@simpleart.eu + - column: + name: first_name + value: password + - column: + name: last_name + value: password + - column: + name: password + value: $2b$12$8/QvqDnMwqK4g9XkzZe0veDReQkJyyAZ/6heLNeyKHB0UjVvtwqT6 + - column: + name: role + value: MANAGER diff --git a/src/main/resources/db/changelog/changes/02-create-user.yaml b/src/main/resources/db/changelog/changes/02-create-user.yaml new file mode 100644 index 0000000..072eab6 --- /dev/null +++ b/src/main/resources/db/changelog/changes/02-create-user.yaml @@ -0,0 +1,23 @@ +databaseChangeLog: + - changeSet: + id: 02-create-user + author: mg + changes: + - insert: + tableName: users + columns: + - column: + name: email + value: customer@simpleart.eu + - column: + name: first_name + value: password + - column: + name: last_name + value: password + - column: + name: password + value: $2b$12$8/QvqDnMwqK4g9XkzZe0veDReQkJyyAZ/6heLNeyKHB0UjVvtwqT6 + - column: + name: role + value: CUSTOMER diff --git a/src/main/resources/db/changelog/changes/create-car-table.yaml b/src/main/resources/db/changelog/changes/create-car-table.yaml new file mode 100644 index 0000000..7e97a52 --- /dev/null +++ b/src/main/resources/db/changelog/changes/create-car-table.yaml @@ -0,0 +1,45 @@ +databaseChangeLog: + - changeSet: + id: create-car-table + author: mg + changes: + - createTable: + tableName: cars + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + - column: + name: model + type: VARCHAR(255) + constraints: + nullable: false + - column: + name: brand + type: VARCHAR(255) + constraints: + nullable: false + - column: + name: type + type: VARCHAR(50) + constraints: + nullable: false + - column: + name: inventory + type: INT + constraints: + nullable: false + - column: + name: daily_fee + type: DECIMAL(10, 2) + constraints: + nullable: false + - column: + name: deleted + type: bit + defaultValueBoolean: false + constraints: + nullable: false \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/create-payment-table.yaml b/src/main/resources/db/changelog/changes/create-payment-table.yaml new file mode 100644 index 0000000..477e4fb --- /dev/null +++ b/src/main/resources/db/changelog/changes/create-payment-table.yaml @@ -0,0 +1,50 @@ +databaseChangeLog: + - changeSet: + id: create-payment-table + author: mg + changes: + - createTable: + tableName: payments + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + - column: + name: status + type: VARCHAR(50) + constraints: + nullable: false + - column: + name: type + type: VARCHAR(50) + constraints: + nullable: false + - column: + name: rental_id + type: BIGINT + constraints: + nullable: false + - column: + name: session_url + type: VARCHAR(510) + constraints: + nullable: false + - column: + name: session_id + type: VARCHAR(255) + constraints: + nullable: false + - column: + name: amount_to_pay + type: DECIMAL(10, 2) + constraints: + nullable: false + - column: + name: deleted + type: bit + defaultValueBoolean: false + constraints: + nullable: false \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/create-rental-table.yaml b/src/main/resources/db/changelog/changes/create-rental-table.yaml new file mode 100644 index 0000000..0436d6f --- /dev/null +++ b/src/main/resources/db/changelog/changes/create-rental-table.yaml @@ -0,0 +1,43 @@ +databaseChangeLog: + - changeSet: + id: create-rental-table + author: mg + changes: + - createTable: + tableName: rentals + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + - column: + name: rental_date + type: DATE + constraints: + nullable: false + - column: + name: return_date + type: DATE + constraints: + nullable: false + - column: + name: actual_return_date + type: DATE + - column: + name: car_id + type: BIGINT + constraints: + nullable: false + - column: + name: user_id + type: BIGINT + constraints: + nullable: false + - column: + name: deleted + type: bit + defaultValueBoolean: false + constraints: + nullable: false \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/create-user-table.yaml b/src/main/resources/db/changelog/changes/create-user-table.yaml new file mode 100644 index 0000000..7a38765 --- /dev/null +++ b/src/main/resources/db/changelog/changes/create-user-table.yaml @@ -0,0 +1,46 @@ +databaseChangeLog: + - changeSet: + id: create-user-table + author: mg + changes: + - createTable: + tableName: users + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + - column: + name: email + type: VARCHAR(255) + constraints: + nullable: false + unique: true + - column: + name: first_name + type: VARCHAR(255) + constraints: + nullable: false + - column: + name: last_name + type: VARCHAR(255) + constraints: + nullable: false + - column: + name: password + type: VARCHAR(255) + constraints: + nullable: false + - column: + name: role + type: VARCHAR(50) + constraints: + nullable: false + - column: + name: deleted + type: bit + defaultValueBoolean: false + constraints: + nullable: false \ No newline at end of file diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml new file mode 100644 index 0000000..9ca9835 --- /dev/null +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -0,0 +1,13 @@ +databaseChangeLog: + - include: + file: db/changelog/changes/create-car-table.yaml + - include: + file: db/changelog/changes/create-user-table.yaml + - include: + file: db/changelog/changes/create-rental-table.yaml + - include: + file: db/changelog/changes/create-payment-table.yaml + - include: + file: db/changelog/changes/01-create-user.yaml + - include: + file: db/changelog/changes/02-create-user.yaml diff --git a/src/test/java/com/marek/carsharing/config/CustomPostgresContainer.java b/src/test/java/com/marek/carsharing/config/CustomPostgresContainer.java new file mode 100644 index 0000000..3af838f --- /dev/null +++ b/src/test/java/com/marek/carsharing/config/CustomPostgresContainer.java @@ -0,0 +1,32 @@ +package com.marek.carsharing.config; + +import org.testcontainers.containers.PostgreSQLContainer; + +public class CustomPostgresContainer extends PostgreSQLContainer { + public static final String DB_IMAGE = "postgres:15.3"; + + private static CustomPostgresContainer postgresContainer; + + public CustomPostgresContainer() { + super(DB_IMAGE); + } + + public static synchronized CustomPostgresContainer getInstance() { + if (postgresContainer == null) { + postgresContainer = new CustomPostgresContainer(); + } + return postgresContainer; + } + + @Override + public void start() { + super.start(); + System.setProperty("TEST_DB_URL", postgresContainer.getJdbcUrl()); + System.setProperty("TEST_DB_USERNAME", postgresContainer.getUsername()); + System.setProperty("TEST_DB_PASSWORD", postgresContainer.getPassword()); + } + + @Override + public void stop() { + } +} diff --git a/src/test/java/com/marek/carsharing/controller/CarsControllerTest.java b/src/test/java/com/marek/carsharing/controller/CarsControllerTest.java new file mode 100644 index 0000000..c08644f --- /dev/null +++ b/src/test/java/com/marek/carsharing/controller/CarsControllerTest.java @@ -0,0 +1,299 @@ +package com.marek.carsharing.controller; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.marek.carsharing.dto.car.CarDetailsDto; +import com.marek.carsharing.dto.car.CarDto; +import com.marek.carsharing.dto.car.CarRequestDto; +import com.marek.carsharing.model.classes.Car; +import com.marek.carsharing.model.enums.Type; +import java.math.BigDecimal; +import java.sql.Connection; +import java.util.Arrays; +import java.util.List; +import javax.sql.DataSource; +import lombok.SneakyThrows; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.jdbc.datasource.init.ScriptUtils; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.testcontainers.shaded.org.apache.commons.lang3.builder.EqualsBuilder; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class CarsControllerTest { + public static final String MODEL = "model"; + public static final String BRAND = "brand"; + public static final String UNIVERSAL = "UNIVERSAL"; + public static final int INVENTORY = 10; + public static final BigDecimal DAILY_FEE = BigDecimal.valueOf(10); + public static final long ID = 1L; + public static final Type UNIVERSAL_TYPE = Type.UNIVERSAL; + private static MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + @Autowired + private WebApplicationContext webApplicationContext; + @Autowired + private DataSource dataSource; + + @BeforeEach + void beforeAll() { + mockMvc = MockMvcBuilders + .webAppContextSetup(webApplicationContext) + .apply(SecurityMockMvcConfigurers.springSecurity()) + .build(); + } + + @AfterEach + void afterAll() { + teardown(dataSource); + } + + @SneakyThrows + private static void teardown(DataSource dataSource) { + try (Connection connection = dataSource.getConnection()) { + connection.setAutoCommit(true); + ScriptUtils.executeSqlScript( + connection, + new ClassPathResource("db/controller/delete-from-cars.sql") + ); + } + } + + @Test + @WithMockUser(username = "MANAGER", authorities = {"MANAGER"}) + @DisplayName("Test adding a new car successfully - MANAGER only") + void addCar_Success() throws Exception { + //given + CarRequestDto carRequestDto = getCarRequestDto(); + CarDto carDto = getCarDto(); + + String jsonRequest = objectMapper.writeValueAsString( + carRequestDto + ); + //when + MvcResult result = mockMvc.perform( + post("/cars") + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(status().isCreated()) + .andReturn(); + //then + + CarDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), + CarDto.class); + + EqualsBuilder.reflectionEquals(carDto, actual, "id"); + } + + @Test + @WithMockUser(username = "admin", authorities = {"MANAGER"}) + @DisplayName("Test retrieving a list of cars successfully - MANAGER & CUSTOMER") + @Sql(scripts = "/db/controller/add-to-cars.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "/db/controller/delete-from-cars.sql", + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void getCars_Success() throws Exception { + //given + Pageable pageable = PageRequest.of(0, 10); + CarDto carDto = getCarDto(); + + String jsonRequest = objectMapper.writeValueAsString( + pageable + ); + //when + MvcResult result = mockMvc.perform( + get("/cars") + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(status().isOk()) + .andReturn(); + //then + List actual = Arrays.asList( + objectMapper.readValue(result.getResponse().getContentAsString(), + CarDto[].class)); + + EqualsBuilder.reflectionEquals(carDto, actual.get(0), "id"); + } + + @Test + @WithMockUser(username = "admin", authorities = {"MANAGER"}) + @DisplayName("Test retrieving detailed information of a car successfully - MANAGER & CUSTOMER") + @Sql(scripts = "/db/controller/add-to-cars.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "/db/controller/delete-from-cars.sql", + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void getCarDetails_Success() throws Exception { + //given + Long id = ID; + CarDetailsDto carDetailsDto = getCarDetailsDto(); + + String jsonRequest = objectMapper.writeValueAsString( + id + ); + //when + MvcResult result = mockMvc.perform( + get("/cars/" + id) + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(status().isOk()) + .andReturn(); + //then + CarDetailsDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), + CarDetailsDto.class); + + EqualsBuilder.reflectionEquals(carDetailsDto, actual, "id"); + } + + @Test + @WithMockUser(username = "admin", authorities = {"MANAGER"}) + @DisplayName("Test updating a car successfully - MANAGER only") + @Sql(scripts = "classpath:db/controller/add-to-cars.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "classpath:db/controller/delete-from-cars.sql", + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void updateCar_Success() throws Exception { + //given + Long id = ID; + CarDto carDto = getCarDto(); + CarRequestDto carRequestDto = getCarRequestDto(); + + String jsonRequest = objectMapper.writeValueAsString( + carRequestDto + ); + //when + MvcResult result = mockMvc.perform( + put("/cars/" + id) + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(status().isOk()) + .andReturn(); + //then + + CarDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), + CarDto.class); + + EqualsBuilder.reflectionEquals(carDto, actual, "id"); + } + + @Test + @WithMockUser(username = "admin", authorities = {"MANAGER"}) + @DisplayName("Test deleting a car successfully - MANAGER only") + void deleteCar_Success() throws Exception { + Car car = getCar(); + Long id = car.getId(); + + mockMvc.perform( + delete("/cars/" + id) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + } + + @Test + @WithMockUser(username = "MANAGER", authorities = {"MANAGER"}) + @DisplayName("Test adding a car with invalid data - MANAGER only") + void addCar_InvalidData() throws Exception { + CarRequestDto carRequestDto = + new CarRequestDto(null, null, null, -1, BigDecimal.ZERO); // Invalid data + + String jsonRequest = objectMapper.writeValueAsString(carRequestDto); + + mockMvc.perform( + post("/cars") + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(username = "admin", authorities = {"MANAGER"}) + @DisplayName("Test retrieving details of a non-existent car") + void getCarDetails_NonExistentCar() throws Exception { + Long nonExistentId = 999L; + + mockMvc.perform( + get("/cars/" + nonExistentId) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(username = "admin", authorities = {"MANAGER"}) + @DisplayName("Test updating a car with invalid data - MANAGER only") + void updateCar_InvalidData() throws Exception { + Long id = ID; + CarRequestDto carRequestDto = + new CarRequestDto(null, null, null, -1, BigDecimal.ZERO); // Invalid data + + String jsonRequest = objectMapper.writeValueAsString(carRequestDto); + + mockMvc.perform( + put("/cars/" + id) + .content(jsonRequest) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); + } + + private CarRequestDto getCarRequestDto() { + return new CarRequestDto( + MODEL, BRAND, UNIVERSAL, INVENTORY, DAILY_FEE + ); + } + + private Car getCar() { + Car car = new Car(); + car.setId(ID); + car.setModel(MODEL); + car.setBrand(BRAND); + car.setType(UNIVERSAL_TYPE); + car.setInventory(INVENTORY); + car.setDailyFee(DAILY_FEE); + car.setDeleted(false); + return car; + } + + private CarDto getCarDto() { + CarDto carDto = new CarDto(); + carDto.setId(ID); + carDto.setModel(MODEL); + carDto.setBrand(BRAND); + carDto.setInventory(INVENTORY); + carDto.setDailyFee(DAILY_FEE); + return carDto; + } + + private CarDetailsDto getCarDetailsDto() { + CarDetailsDto carDetailsDto = new CarDetailsDto(); + carDetailsDto.setId(ID); + carDetailsDto.setModel(MODEL); + carDetailsDto.setBrand(BRAND); + carDetailsDto.setType(UNIVERSAL); + carDetailsDto.setInventory(INVENTORY); + carDetailsDto.setDailyFee(DAILY_FEE); + return carDetailsDto; + } +} + diff --git a/src/test/java/com/marek/carsharing/controller/PaymentsControllerTest.java b/src/test/java/com/marek/carsharing/controller/PaymentsControllerTest.java new file mode 100644 index 0000000..17d479b --- /dev/null +++ b/src/test/java/com/marek/carsharing/controller/PaymentsControllerTest.java @@ -0,0 +1,209 @@ +package com.marek.carsharing.controller; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.marek.carsharing.dto.payment.CreatePaymentRequestDto; +import com.marek.carsharing.dto.payment.PaymentDto; +import com.marek.carsharing.dto.payment.PaymentSearchParameters; +import java.math.BigDecimal; +import java.sql.Connection; +import java.util.Arrays; +import java.util.List; +import javax.sql.DataSource; +import lombok.SneakyThrows; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.MediaType; +import org.springframework.jdbc.datasource.init.ScriptUtils; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.testcontainers.shaded.org.apache.commons.lang3.builder.EqualsBuilder; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class PaymentsControllerTest { + public static final String[] USERS_ID = {"1"}; + public static final long ID = 1L; + public static final String SESSION_URL = "asfasnfkasf"; + public static final String SESSION_ID = "123124124"; + public static final BigDecimal AMOUNT_TO_PAY = BigDecimal.valueOf(100); + private static MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @BeforeAll + static void beforeAll(@Autowired WebApplicationContext applicationContext) { + mockMvc = MockMvcBuilders + .webAppContextSetup(applicationContext) + .apply(SecurityMockMvcConfigurers.springSecurity()) + .build(); + } + + @AfterAll + static void afterAll(@Autowired DataSource dataSource) { + teardown(dataSource); + } + + @SneakyThrows + private static void teardown(DataSource dataSource) { + try (Connection connection = dataSource.getConnection()) { + connection.setAutoCommit(true); + ScriptUtils.executeSqlScript( + connection, + new ClassPathResource("db/controller/delete-from-payments.sql") + ); + } + } + + @Test + @WithMockUser(username = "user", authorities = {"MANAGER"}) + @DisplayName("Test for successful retrieval of payments") + @Sql(scripts = "classpath:db/controller/add-to-payments.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "classpath:db/controller/delete-from-payments.sql", + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @SneakyThrows + void getPayments_Success() { + // Given + PaymentSearchParameters paymentSearchParameters = getPaymentSearchParameters(); + PaymentDto paymentDto = getPaymentDto(); + + String json = objectMapper.writeValueAsString(paymentSearchParameters); + // When + MvcResult result = mockMvc.perform( + MockMvcRequestBuilders.get("/payments") + .content(json) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andReturn(); + // Then + List list = Arrays.asList( + objectMapper.readValue(result.getResponse().getContentAsString(), + PaymentDto[].class) + ); + EqualsBuilder.reflectionEquals(paymentDto, list.get(0), "id"); + } + + @Test + @WithUserDetails(value = "admin@simpleart.eu") + @DisplayName("Test for successful creation of payment session") + @Sql(scripts = {"classpath:db/controller/add-to-rentals.sql", + "classpath:db/controller/add-to-cars.sql", + "classpath:db/controller/delete-from-users.sql", + "classpath:db/controller/add-to-users.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:db/controller/delete-from-rentals.sql", + "classpath:db/controller/delete-from-cars.sql", + "classpath:db/controller/delete-from-users.sql"}, + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @SneakyThrows + void createPaymentSession_Success() { + // Given + CreatePaymentRequestDto createPaymentRequestDto = getCreatePaymentRequestDto(); + PaymentDto paymentDto = getPaymentDto(); + + String json = objectMapper.writeValueAsString(createPaymentRequestDto); + // When + MvcResult result = mockMvc.perform( + MockMvcRequestBuilders.post("/payments") + .content(json) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()) + .andReturn(); + // Then + PaymentDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), + PaymentDto.class); + EqualsBuilder.reflectionEquals(paymentDto, actual, "id"); + } + + @Test + @WithMockUser(username = "user", authorities = {"MANAGER"}) + @DisplayName("Test for invalid search parameters") + @SneakyThrows + void getPayments_InvalidSearchParameters() { + PaymentSearchParameters invalidSearchParameters = + new PaymentSearchParameters(null); + + String json = objectMapper.writeValueAsString(invalidSearchParameters); + mockMvc.perform( + MockMvcRequestBuilders.get("/payments") + .content(json) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(username = "user", authorities = {"USER"}) + @DisplayName("Test for unauthorized access to create payment session") + @SneakyThrows + void createPaymentSession_Unauthorized() { + CreatePaymentRequestDto createPaymentRequestDto = getCreatePaymentRequestDto(); + String json = objectMapper.writeValueAsString(createPaymentRequestDto); + + mockMvc.perform( + MockMvcRequestBuilders.post("/payments") + .content(json) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isForbidden()); + } + + @Test + @WithMockUser(username = "user", authorities = {"MANAGER"}) + @DisplayName("Test for retrieving non-existent payments") + @SneakyThrows + void getPayments_NonExistentResource() { + PaymentSearchParameters paymentSearchParameters = + new PaymentSearchParameters(new String[]{"9999"}); // Non-existent user ID + + String json = objectMapper.writeValueAsString(paymentSearchParameters); + + mockMvc.perform( + MockMvcRequestBuilders.get("/payments") + .content(json) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNotFound()); + } + + private PaymentDto getPaymentDto() { + PaymentDto paymentDto = new PaymentDto(); + paymentDto.setId(ID); + paymentDto.setRentalId(ID); + paymentDto.setSessionUrl(SESSION_URL); + paymentDto.setSessionId(SESSION_ID); + paymentDto.setAmountToPay(AMOUNT_TO_PAY); + return paymentDto; + } + + private PaymentSearchParameters getPaymentSearchParameters() { + PaymentSearchParameters paymentSearchParameters = new PaymentSearchParameters( + USERS_ID + ); + return paymentSearchParameters; + } + + private CreatePaymentRequestDto getCreatePaymentRequestDto() { + CreatePaymentRequestDto createPaymentRequestDto = + new CreatePaymentRequestDto( + ID + ); + return createPaymentRequestDto; + } +} diff --git a/src/test/java/com/marek/carsharing/controller/RentalsControllerTest.java b/src/test/java/com/marek/carsharing/controller/RentalsControllerTest.java new file mode 100644 index 0000000..408f6ff --- /dev/null +++ b/src/test/java/com/marek/carsharing/controller/RentalsControllerTest.java @@ -0,0 +1,264 @@ +package com.marek.carsharing.controller; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.marek.carsharing.dto.rental.CreateRentalRequestDto; +import com.marek.carsharing.dto.rental.RentalDto; +import com.marek.carsharing.dto.rental.RentalSearchParameters; +import com.marek.carsharing.model.classes.Rental; +import com.marek.carsharing.model.classes.User; +import com.marek.carsharing.model.enums.Role; +import java.sql.Connection; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; +import javax.sql.DataSource; +import lombok.SneakyThrows; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.MediaType; +import org.springframework.jdbc.datasource.init.ScriptUtils; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.testcontainers.shaded.org.apache.commons.lang3.builder.EqualsBuilder; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class RentalsControllerTest { + public static final long ID = 1L; + public static final String EMAIL = "admin@simpleart.eu"; + public static final String FIRST_NAME = "Admin"; + public static final String LAST_NAME = "User"; + public static final String PASSWORD = "password"; + public static final Role ROLE = Role.MANAGER; + public static final LocalDate RENTAL_DATE = LocalDate.of(2024, 8, 1); + public static final LocalDate RETURN_DATE = LocalDate.of(2024, 9, 29); + public static final LocalDate ACTUAL_RETURN_DATE = LocalDate.of(2024, 8, 5); + public static final String[] USER_ID = {"1"}; + public static final boolean IS_ACTIVE = true; + private static MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + @Autowired + private WebApplicationContext webApplicationContext; + @Autowired + private DataSource dataSource; + + @BeforeEach + void beforeAll() { + mockMvc = MockMvcBuilders + .webAppContextSetup(webApplicationContext) + .apply(SecurityMockMvcConfigurers.springSecurity()) + .build(); + } + + @AfterEach + void afterAll() { + teardown(dataSource); + } + + @SneakyThrows + private static void teardown(DataSource dataSource) { + try (Connection connection = dataSource.getConnection()) { + connection.setAutoCommit(true); + ScriptUtils.executeSqlScript( + connection, + new ClassPathResource("db/controller/delete-from-rentals.sql") + ); + } + } + + @Test + @WithUserDetails(value = "admin@simpleart.eu") + @Sql(scripts = {"classpath:db/controller/add-to-cars.sql", + "classpath:db/controller/add-to-users.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:db/controller/delete-from-cars.sql", + "classpath:db/controller/delete-from-users.sql"}, + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @DisplayName("Test adding a rental successfully") + void addRental_Success() throws Exception { + CreateRentalRequestDto createRentalRequestDto = getCreateRentalRequestDto(); + RentalDto rentalDto = getRentalDto(); + + String json = objectMapper + .writeValueAsString(createRentalRequestDto); + + MvcResult result = mockMvc.perform( + MockMvcRequestBuilders.post("/rentals") + .content(json) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()) + .andReturn(); + + RentalDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), + RentalDto.class); + + EqualsBuilder.reflectionEquals(rentalDto, actual, "id"); + } + + @Test + @WithMockUser(username = "user", authorities = {"MANAGER"}) + @Sql(scripts = {"classpath:db/controller/add-to-cars.sql", + "classpath:db/controller/add-to-rentals.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:db/controller/delete-from-cars.sql", + "classpath:db/controller/delete-from-rentals.sql"}, + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @DisplayName("Test getting all rentals successfully with search parameters") + void getRentals_Success() throws Exception { + RentalSearchParameters parameters = getRentalSearchParameters(); + + RentalDto rentalDto = getRentalDto(); + + String json = objectMapper.writeValueAsString(parameters); + + MvcResult result = mockMvc.perform( + get("/rentals") + .content(json) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andReturn(); + + List actual = Arrays.asList( + objectMapper.readValue(result.getResponse().getContentAsString(), + RentalDto[].class)); + + EqualsBuilder.reflectionEquals(rentalDto, actual.get(0), "id"); + } + + @Test + @WithMockUser(username = "user", authorities = {"MANAGER"}) + @DisplayName("Test getting specific rental successfully - MANAGER only") + @Sql(scripts = {"classpath:db/controller/add-to-rentals.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:db/controller/delete-from-rentals.sql"}, + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void getRental_Success() throws Exception { + //given + RentalDto rentalDto = getRentalDto(); + Long id = rentalDto.getId(); + + //when + MvcResult result = mockMvc.perform( + get("/rentals/" + id) + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(status().isOk()) + .andReturn(); + //then + RentalDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), + RentalDto.class); + + EqualsBuilder.reflectionEquals(rentalDto, actual, "id"); + } + + @Test + @WithUserDetails(value = "admin@simpleart.eu") + @Sql(scripts = {"classpath:db/controller/add-to-rentals.sql", + "classpath:db/controller/add-to-cars.sql", + "classpath:db/controller/add-to-users.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:db/controller/delete-from-rentals.sql", + "classpath:db/controller/delete-from-cars.sql"}, + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @DisplayName("Test returning a rental successfully") + void returnRental_Success() throws Exception { + long id = ID; + + mockMvc.perform( + MockMvcRequestBuilders.post("/rentals/" + id + "/return") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andReturn(); + + } + + @Test + @WithMockUser(username = "user", authorities = {"MANAGER"}) + @DisplayName("Test getting specific rental with invalid ID") + void getRental_InvalidId() throws Exception { + Long invalidId = 999L; + + mockMvc.perform( + get("/rentals/" + invalidId) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNotFound()); + } + + @Test + @WithUserDetails(value = "admin@simpleart.eu") + @DisplayName("Test returning a rental with invalid ID") + void returnRental_InvalidId() throws Exception { + long invalidId = 999L; + + mockMvc.perform( + MockMvcRequestBuilders.post("/rentals/" + invalidId + "/return") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNotFound()); + } + + private User getUser() { + User user = new User(); + user.setId(ID); + user.setEmail(EMAIL); + user.setFirstName(FIRST_NAME); + user.setLastName(LAST_NAME); + user.setPassword(PASSWORD); + user.setRole(ROLE); + user.setDeleted(false); + return user; + } + + private CreateRentalRequestDto getCreateRentalRequestDto() { + return new CreateRentalRequestDto( + RENTAL_DATE, RETURN_DATE, ID + ); + } + + private Rental getRental() { + Rental rental = new Rental(); + rental.setId(ID); + rental.setRentalDate(RENTAL_DATE); + rental.setReturnDate(RETURN_DATE); + rental.setActualReturnDate(ACTUAL_RETURN_DATE); + rental.setCarId(ID); + rental.setUserId(ID); + rental.setDeleted(false); + return rental; + } + + private RentalDto getRentalDto() { + RentalDto rentalDto = new RentalDto(); + rentalDto.setId(ID); + rentalDto.setRentalDate(RENTAL_DATE); + rentalDto.setActualReturnDate(ACTUAL_RETURN_DATE); + rentalDto.setReturnDate(RETURN_DATE); + rentalDto.setCarId(ID); + return rentalDto; + } + + private RentalSearchParameters getRentalSearchParameters() { + RentalSearchParameters parameters = new RentalSearchParameters(USER_ID, IS_ACTIVE); + return parameters; + } +} + diff --git a/src/test/java/com/marek/carsharing/controller/UsersControllerTest.java b/src/test/java/com/marek/carsharing/controller/UsersControllerTest.java new file mode 100644 index 0000000..e38e017 --- /dev/null +++ b/src/test/java/com/marek/carsharing/controller/UsersControllerTest.java @@ -0,0 +1,216 @@ +package com.marek.carsharing.controller; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.marek.carsharing.dto.user.UpdateUserRequestDto; +import com.marek.carsharing.dto.user.UserDto; +import com.marek.carsharing.model.classes.User; +import com.marek.carsharing.model.enums.Role; +import java.sql.Connection; +import javax.sql.DataSource; +import lombok.SneakyThrows; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.MediaType; +import org.springframework.jdbc.datasource.init.ScriptUtils; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.testcontainers.shaded.org.apache.commons.lang3.builder.EqualsBuilder; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class UsersControllerTest { + public static final long ID = 1L; + public static final String EMAIL = "admin@simpleart.eu"; + public static final String FIRST_NAME = "Admin"; + public static final String LAST_NAME = "User"; + public static final String PASSWORD = "password"; + public static final Role ROLE = Role.MANAGER; + private static MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + @Autowired + private WebApplicationContext webApplicationContext; + @Autowired + private DataSource dataSource; + + @BeforeEach + void beforeAll() { + mockMvc = MockMvcBuilders + .webAppContextSetup(webApplicationContext) + .apply(SecurityMockMvcConfigurers.springSecurity()) + .build(); + } + + @AfterEach + void afterAll() { + teardown(dataSource); + } + + @SneakyThrows + private static void teardown(DataSource dataSource) { + try (Connection connection = dataSource.getConnection()) { + connection.setAutoCommit(true); + ScriptUtils.executeSqlScript( + connection, + new ClassPathResource("db/controller/delete-from-users.sql") + ); + } + } + + @Test + @WithMockUser(username = "user", authorities = {"MANAGER"}) + @DisplayName("Test updating user role successfully - MANAGER only") + @Sql(scripts = {"classpath:db/controller/delete-from-users.sql", + "classpath:db/controller/add-to-users.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:db/controller/delete-from-users.sql"}, + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void updateUserRole_Success() throws Exception { + //given + UserDto userDto = getUserDto(); + Long id = userDto.getId(); + + //when + MvcResult result = mockMvc.perform( + put("/users/" + id + "/role") + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(status().isOk()) + .andReturn(); + + //then + UserDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), + UserDto.class); + + EqualsBuilder.reflectionEquals(userDto, actual, "id"); + } + + @Test + @WithUserDetails(value = "admin@simpleart.eu") + @Sql(scripts = {"classpath:db/controller/delete-from-users.sql", + "classpath:db/controller/add-to-users.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:db/controller/delete-from-users.sql"}, + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @DisplayName("Get the profile of the currently authenticated user successfully") + void getMyProfile_Success() throws Exception { + UserDto userDto = getUserDto(); + + MvcResult result = mockMvc.perform( + MockMvcRequestBuilders.get("/users/me") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andReturn(); + UserDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), + UserDto.class); + + EqualsBuilder.reflectionEquals(userDto, actual, "id"); + } + + @Test + @WithUserDetails(value = "admin@simpleart.eu") + @Sql(scripts = {"classpath:db/controller/add-to-users.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:db/controller/delete-from-users.sql"}, + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @DisplayName("Update own profile successfully") + void updateMyProfile_Success() throws Exception { + UpdateUserRequestDto updateUserRequestDto = getUpdateUserRequestDto(); + UserDto userDto = getUserDto(); + + String json = objectMapper.writeValueAsString(updateUserRequestDto); + + MvcResult result = mockMvc.perform( + MockMvcRequestBuilders.put("/users/me") + .content(json) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andReturn(); + UserDto actual = objectMapper.readValue(result.getResponse().getContentAsString(), + UserDto.class); + + EqualsBuilder.reflectionEquals(userDto, actual, "id"); + + } + + @Test + @WithMockUser(username = "user", authorities = {"USER"}) + @DisplayName("Test updating user role without MANAGER authority") + @Sql(scripts = {"classpath:db/controller/delete-from-users.sql", + "classpath:db/controller/add-to-users.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:db/controller/delete-from-users.sql"}, + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void updateUserRole_Fail_Unauthorized() throws Exception { + Long id = ID; + + mockMvc.perform( + put("/users/" + id + "/role") + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(status().isForbidden()); + } + + @Test + @WithUserDetails(value = "admin@simpleart.eu") + @DisplayName("Test updating own profile with invalid data") + @Sql(scripts = {"classpath:db/controller/delete-from-users.sql", + "classpath:db/controller/add-to-users.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:db/controller/delete-from-users.sql"}, + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void updateMyProfile_Fail_InvalidData() throws Exception { + UpdateUserRequestDto invalidRequestDto = new UpdateUserRequestDto("", ""); + + String json = objectMapper.writeValueAsString(invalidRequestDto); + + mockMvc.perform( + MockMvcRequestBuilders.put("/users/me") + .content(json) + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(status().isBadRequest()); + } + + private User getUser() { + User user = new User(); + user.setId(ID); + user.setEmail(EMAIL); + user.setFirstName(FIRST_NAME); + user.setLastName(LAST_NAME); + user.setPassword(PASSWORD); + user.setRole(ROLE); + user.setDeleted(false); + return user; + } + + private UserDto getUserDto() { + UserDto userDto = new UserDto(); + userDto.setId(ID); + userDto.setEmail(EMAIL); + userDto.setFirstName(FIRST_NAME); + userDto.setLastName(LAST_NAME); + userDto.setRole(String.valueOf(ROLE)); + return userDto; + } + + private UpdateUserRequestDto getUpdateUserRequestDto() { + return new UpdateUserRequestDto( + FIRST_NAME, LAST_NAME + ); + } +} diff --git a/src/test/java/com/marek/carsharing/repository/rental/RentalRepositoryTest.java b/src/test/java/com/marek/carsharing/repository/rental/RentalRepositoryTest.java new file mode 100644 index 0000000..3a4d10a --- /dev/null +++ b/src/test/java/com/marek/carsharing/repository/rental/RentalRepositoryTest.java @@ -0,0 +1,59 @@ +package com.marek.carsharing.repository.rental; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.marek.carsharing.model.classes.Rental; +import java.time.LocalDate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.jdbc.Sql; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class RentalRepositoryTest { + @Autowired + private RentalRepository rentalRepository; + + @Test + @DisplayName(""" + """) + @Sql(scripts = {"classpath:db/repo/clean-rental-repo.sql", + "classpath:db/repo/add-to-rental-repo.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:db/repo/clean-rental-repo.sql"}, + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void findByUserId_getOptionalOfRentalFromUserId() { + Long userId = 1L; + Rental testRental = getRental(); + Rental rental = rentalRepository.findByUserId(userId).orElse(null); + + assertAll( + () -> assertNotNull(rental), + () -> assertEquals(rental.getUserId(), userId), + () -> assertEquals(testRental.getRentalDate(), rental.getRentalDate()), + () -> assertEquals(testRental.getUserId(), rental.getUserId()), + () -> assertEquals(testRental.getCarId(), rental.getCarId()), + () -> assertEquals( + testRental.getActualReturnDate(), rental.getActualReturnDate()), + () -> assertEquals(testRental.getReturnDate(), rental.getReturnDate()) + ); + } + + private Rental getRental() { + Rental rental = new Rental(); + rental.setId(1L); + rental.setRentalDate(LocalDate.of(2024, 8, 1)); + rental.setReturnDate(LocalDate.of(2024, 8, 5)); + rental.setActualReturnDate(LocalDate.of(2024, 8, 5)); + rental.setCarId(1L); + rental.setUserId(1L); + rental.setDeleted(false); + return rental; + } +} + diff --git a/src/test/java/com/marek/carsharing/repository/user/UserRepositoryTest.java b/src/test/java/com/marek/carsharing/repository/user/UserRepositoryTest.java new file mode 100644 index 0000000..f15582f --- /dev/null +++ b/src/test/java/com/marek/carsharing/repository/user/UserRepositoryTest.java @@ -0,0 +1,58 @@ +package com.marek.carsharing.repository.user; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.marek.carsharing.model.classes.User; +import com.marek.carsharing.model.enums.Role; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.jdbc.Sql; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class UserRepositoryTest { + @Autowired + private UserRepository userRepository; + + @Test + @DisplayName(""" + """) + @Sql(scripts = {"classpath:db/repo/clean-user-repo.sql", + "classpath:db/repo/add-to-user-repo.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:db/repo/clean-user-repo.sql"}, + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void findUserByEmail_getOptionalOfUserFromEmail() { + String email = "admin@simpleart.eu"; + User testUser = getUser(); + + User user = userRepository.findUserByEmail(email).orElse(null); + + assertAll( + () -> assertNotNull(user), + () -> assertEquals(email, user.getEmail()), + () -> assertEquals(testUser.getId(), user.getId()), + () -> assertEquals(testUser.getEmail(), user.getEmail()), + () -> assertEquals(testUser.getRole(), user.getRole()), + () -> assertEquals(testUser.getFirstName(), user.getFirstName()), + () -> assertEquals(testUser.getLastName(), user.getLastName()) + ); + } + + private User getUser() { + User user = new User(); + user.setId(1L); + user.setFirstName("B"); + user.setLastName("C"); + user.setRole(Role.MANAGER); + user.setPassword("password"); + user.setEmail("admin@simpleart.eu"); + user.setDeleted(false); + return user; + } +} diff --git a/src/test/java/com/marek/carsharing/service/car/CarServiceImplTest.java b/src/test/java/com/marek/carsharing/service/car/CarServiceImplTest.java new file mode 100644 index 0000000..3fbb582 --- /dev/null +++ b/src/test/java/com/marek/carsharing/service/car/CarServiceImplTest.java @@ -0,0 +1,242 @@ +package com.marek.carsharing.service.car; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.marek.carsharing.dto.car.CarDetailsDto; +import com.marek.carsharing.dto.car.CarDto; +import com.marek.carsharing.dto.car.CarRequestDto; +import com.marek.carsharing.mapper.CarMapper; +import com.marek.carsharing.model.classes.Car; +import com.marek.carsharing.model.enums.Type; +import com.marek.carsharing.repository.car.CarRepository; +import jakarta.persistence.EntityNotFoundException; +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; +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.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +@ExtendWith(MockitoExtension.class) +class CarServiceImplTest { + public static final String MODEL = "model"; + public static final String BRAND = "brand"; + public static final String UNIVERSAL = "UNIVERSAL"; + public static final int INVENTORY = 10; + public static final BigDecimal DAILY_FEE = BigDecimal.valueOf(10); + public static final long ID = 1L; + public static final Type UNIVERSAL_TYPE = Type.UNIVERSAL; + @Mock + private CarRepository carRepository; + + @Mock + private CarMapper carMapper; + + @InjectMocks + private CarServiceImpl carService; + + @Test + @DisplayName("addCar_withValidInput_returnsCarDto") + void addCar_withValidInput_returnsCarDto() { + // Given + CarRequestDto carRequestDto = getCarRequestDto(); + Car car = getCar(); + CarDto carDto = getCarDto(); + + when(carMapper.toEntity(carRequestDto)).thenReturn(car); + when(carRepository.save(car)).thenReturn(car); + when(carMapper.toDto(car)).thenReturn(carDto); + + // When + CarDto result = carService.addCar(carRequestDto); + + // Then + assertNotNull(result); + assertEquals(carDto, result); + verify(carMapper, times(1)).toEntity(carRequestDto); + verify(carRepository, times(1)).save(car); + verify(carMapper, times(1)).toDto(car); + verifyNoMoreInteractions(carMapper, carRepository); + } + + @Test + @DisplayName("getCars_withValidPageable_returnsListOfCarDto") + void getCars_withValidPageable_returnsListOfCarDto() { + // Given + Car car = getCar(); + CarDto carDto = getCarDto(); + Pageable pageable = PageRequest.of(0, 10); + List cars = List.of(car); + Page carPage = new PageImpl<>( + cars, pageable, cars.size() + ); + + when(carRepository.findAll(pageable)).thenReturn(carPage); + when(carMapper.toDto(car)).thenReturn(carDto); + + // When + List result = carService.getCars(pageable); + + // Then + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(carDto, result.get(0)); + verify(carRepository, times(1)).findAll(pageable); + verify(carMapper, times(1)).toDto(car); + verifyNoMoreInteractions(carRepository, carMapper); + } + + @Test + @DisplayName("getCarDetails_withValidId_returnsCarDetailsDto") + void getCarDetails_withValidId_returnsCarDetailsDto() { + // Given + Long carId = 1L; + Car car = getCar(); + CarDetailsDto carDetailsDto = getCarDetailsDto(); + + when(carRepository.findById(carId)).thenReturn(Optional.of(car)); + when(carMapper.toDetailsDto(car)).thenReturn(carDetailsDto); + + // When + CarDetailsDto result = carService.getCarDetails(carId); + + // Then + assertNotNull(result); + assertEquals(carDetailsDto, result); + verify(carRepository, times(1)).findById(carId); + verify(carMapper, times(1)).toDetailsDto(car); + verifyNoMoreInteractions(carRepository, carMapper); + } + + @Test + @DisplayName("getCarDetails_withInvalidId_throwsEntityNotFoundException") + void getCarDetails_withInvalidId_throwsEntityNotFoundException() { + // Given + Long carId = -1L; + + when(carRepository.findById(carId)).thenReturn(Optional.empty()); + + // When & Then + EntityNotFoundException entityNotFoundException = + assertThrows(EntityNotFoundException.class, () -> carService.getCarDetails(carId)); + assertEquals( + "Car not found with id: " + carId, entityNotFoundException.getMessage()); + verify(carRepository, times(1)).findById(carId); + verifyNoMoreInteractions(carRepository); + } + + @Test + @DisplayName("updateCar_withValidIdAndRequest_returnsUpdatedCarDto") + void updateCar_withValidIdAndRequest_returnsUpdatedCarDto() { + // Given + Long carId = 1L; + CarRequestDto carRequestDto = getCarRequestDto(); + Car car = getCar(); + CarDto carDto = getCarDto(); + + when(carRepository.findById(carId)).thenReturn(Optional.of(car)); + doNothing().when(carMapper).updateDto(carRequestDto, car); + when(carRepository.save(car)).thenReturn(car); + when(carMapper.toDto(car)).thenReturn(carDto); + + // When + CarDto result = carService.updateCar(carId, carRequestDto); + + // Then + assertNotNull(result); + assertEquals(carDto, result); + verify(carRepository, times(1)).findById(carId); + verify(carMapper, times(1)).updateDto(carRequestDto, car); + verify(carRepository, times(1)).save(car); + verify(carMapper, times(1)).toDto(car); + verifyNoMoreInteractions(carRepository, carMapper); + } + + @Test + @DisplayName("updateCar_withInvalidId_throwsEntityNotFoundException") + void updateCar_withInvalidId_throwsEntityNotFoundException() { + // Given + Long carId = -1L; + CarRequestDto carRequestDto = getCarRequestDto(); + + when(carRepository.findById(carId)).thenReturn(Optional.empty()); + + // When & Then + EntityNotFoundException entityNotFoundException = + assertThrows(EntityNotFoundException.class, + () -> carService.updateCar(carId, carRequestDto)); + + assertEquals( + "Car not found with id: " + carId, entityNotFoundException.getMessage()); + verify(carRepository, times(1)).findById(carId); + verifyNoMoreInteractions(carRepository); + } + + @Test + @DisplayName("deleteCar_withValidId_deletesCar") + void deleteCar_withValidId_deletesCar() { + // Given + Long carId = 1L; + + // When + carService.delete(carId); + + // Then + verify(carRepository, times(1)).deleteById(carId); + verifyNoMoreInteractions(carRepository); + } + + private CarRequestDto getCarRequestDto() { + return new CarRequestDto( + MODEL, BRAND, UNIVERSAL, INVENTORY, DAILY_FEE + ); + } + + private Car getCar() { + Car car = new Car(); + car.setId(ID); + car.setModel(MODEL); + car.setBrand(BRAND); + car.setType(UNIVERSAL_TYPE); + car.setInventory(INVENTORY); + car.setDailyFee(DAILY_FEE); + car.setDeleted(false); + return car; + } + + private CarDto getCarDto() { + CarDto carDto = new CarDto(); + carDto.setId(ID); + carDto.setModel(MODEL); + carDto.setBrand(BRAND); + carDto.setInventory(INVENTORY); + carDto.setDailyFee(DAILY_FEE); + return carDto; + } + + private CarDetailsDto getCarDetailsDto() { + CarDetailsDto carDetailsDto = new CarDetailsDto(); + carDetailsDto.setId(ID); + carDetailsDto.setModel(MODEL); + carDetailsDto.setBrand(BRAND); + carDetailsDto.setType(UNIVERSAL); + carDetailsDto.setInventory(INVENTORY); + carDetailsDto.setDailyFee(DAILY_FEE); + return carDetailsDto; + } +} + diff --git a/src/test/java/com/marek/carsharing/service/rental/RentalServiceImplTest.java b/src/test/java/com/marek/carsharing/service/rental/RentalServiceImplTest.java new file mode 100644 index 0000000..cdc8e90 --- /dev/null +++ b/src/test/java/com/marek/carsharing/service/rental/RentalServiceImplTest.java @@ -0,0 +1,230 @@ +package com.marek.carsharing.service.rental; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.marek.carsharing.dto.rental.CreateRentalRequestDto; +import com.marek.carsharing.dto.rental.RentalDto; +import com.marek.carsharing.mapper.RentalMapper; +import com.marek.carsharing.model.classes.Car; +import com.marek.carsharing.model.classes.Rental; +import com.marek.carsharing.model.classes.User; +import com.marek.carsharing.model.enums.Role; +import com.marek.carsharing.model.enums.Type; +import com.marek.carsharing.repository.car.CarRepository; +import com.marek.carsharing.repository.rental.RentalRepository; +import com.marek.carsharing.service.notification.NotificationService; +import jakarta.persistence.EntityNotFoundException; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Optional; +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; + +@ExtendWith(MockitoExtension.class) +class RentalServiceImplTest { + public static final long ID = 1L; + public static final String EMAIL = "admin@simpleart.eu"; + public static final String FIRST_NAME = "B"; + public static final String LAST_NAME = "W"; + public static final String PASSWORD = "password"; + public static final Role ROLE = Role.MANAGER; + public static final LocalDate RENTAL_DATE = LocalDate.of(2024, 8, 1); + public static final LocalDate RETURN_DATE = LocalDate.of(2024, 8, 5); + public static final LocalDate ACTUAL_RETURN_DATE = LocalDate.of(2024, 8, 5); + public static final String MODEL = "model"; + public static final String BRAND = "brand"; + public static final int INVENTORY = 10; + public static final BigDecimal DAILY_FEE = BigDecimal.valueOf(10); + public static final Type UNIVERSAL_TYPE = Type.UNIVERSAL; + + @Mock + private RentalRepository rentalRepository; + + @Mock + private RentalMapper rentalMapper; + + @Mock + private CarRepository carRepository; + + @Mock + private NotificationService notificationService; + + @InjectMocks + private RentalServiceImpl rentalService; + + @Test + @DisplayName("addRental_withValidInput_returnsRentalDto") + void addRental_withValidInput_returnsRentalDto() { + // Given + User user = getUser(); + CreateRentalRequestDto requestDto = getCreateRentalRequestDto(); + Rental rental = getRental(); + Car car = getCar(); + RentalDto rentalDto = getRentalDto(); + + when(carRepository.findById(car.getId())).thenReturn(Optional.of(car)); + when(rentalMapper.toEntity(requestDto)).thenReturn(rental); + when(rentalRepository.save(rental)).thenReturn(rental); + when(rentalMapper.toDto(rental)).thenReturn(rentalDto); + + // When + RentalDto result = rentalService.addRental(user, requestDto); + + // Then + assertNotNull(result); + assertEquals(rentalDto, result); + verify(carRepository, times(1)).findById(car.getId()); + verify(rentalRepository, times(1)).save(rental); + verify(rentalMapper, times(1)).toEntity(requestDto); + verify(rentalMapper, times(1)).toDto(rental); + verify(notificationService, times(1)).notifyNewRentalsCreated(anyString()); + verifyNoMoreInteractions( + carRepository, rentalRepository, rentalMapper, notificationService); + } + + @Test + @DisplayName("getRental_withValidId_returnsRentalDto") + void getRental_withValidId_returnsRentalDto() { + // Given + Long rentalId = 1L; + Rental rental = getRental(); + RentalDto rentalDto = getRentalDto(); + + when(rentalRepository.findById(rentalId)).thenReturn(Optional.of(rental)); + when(rentalMapper.toDto(rental)).thenReturn(rentalDto); + + // When + RentalDto result = rentalService.getRental(rentalId); + + // Then + assertNotNull(result); + assertEquals(rentalDto, result); + verify(rentalRepository, times(1)).findById(rentalId); + verify(rentalMapper, times(1)).toDto(rental); + verifyNoMoreInteractions(rentalRepository, rentalMapper); + } + + @Test + @DisplayName("getRental_withInvalidId_throwsEntityNotFoundException") + void getRental_withInvalidId_throwsEntityNotFoundException() { + // Given + Long rentalId = -1L; + + when(rentalRepository.findById(rentalId)).thenReturn(Optional.empty()); + + // When & Then + EntityNotFoundException entityNotFoundException = + Assertions.assertThrows( + EntityNotFoundException.class, () -> rentalService.getRental(rentalId)); + Assertions.assertEquals("Rental with id " + rentalId + " not found", + entityNotFoundException.getMessage()); + verify(rentalRepository, times(1)).findById(rentalId); + verifyNoMoreInteractions(rentalRepository); + } + + @Test + @DisplayName("returnRental_withValidInput_updatesCarInventoryAndSavesRental") + void returnRental_withValidInput_updatesCarInventoryAndSavesRental() { + // Given + User user = getUser(); + Long rentalId = 1L; + Rental rental = getRental(); + Car car = getCar(); + + when(rentalRepository.findById(rentalId)).thenReturn(Optional.of(rental)); + when(carRepository.findById(car.getId())).thenReturn(Optional.of(car)); + + // When + rentalService.returnRental(user, rentalId); + + // Then + verify(rentalRepository, times(1)).findById(rentalId); + verify(carRepository, times(1)).findById(anyLong()); + verify(rentalRepository, times(1)).save(rental); + verifyNoMoreInteractions(rentalRepository, carRepository); + } + + @Test + @DisplayName("returnRental_withInvalidRentalId_throwsEntityNotFoundException") + void returnRental_withInvalidRentalId_throwsEntityNotFoundException() { + // Given + User user = getUser(); + Long rentalId = -1L; + + when(rentalRepository.findById(rentalId)).thenReturn(Optional.empty()); + + // When & Then + EntityNotFoundException entityNotFoundException = + Assertions.assertThrows(EntityNotFoundException.class, + () -> rentalService.returnRental(user, rentalId)); + Assertions.assertEquals("Rental with id " + rentalId + " not found", + entityNotFoundException.getMessage()); + verify(rentalRepository, times(1)).findById(rentalId); + verifyNoMoreInteractions(rentalRepository); + } + + private User getUser() { + User user = new User(); + user.setId(ID); + user.setEmail(EMAIL); + user.setFirstName(FIRST_NAME); + user.setLastName(LAST_NAME); + user.setPassword(PASSWORD); + user.setRole(ROLE); + user.setDeleted(false); + return user; + } + + private CreateRentalRequestDto getCreateRentalRequestDto() { + return new CreateRentalRequestDto( + RENTAL_DATE, RETURN_DATE, ID + ); + } + + private Rental getRental() { + Rental rental = new Rental(); + rental.setId(ID); + rental.setRentalDate(RENTAL_DATE); + rental.setReturnDate(RETURN_DATE); + rental.setActualReturnDate(ACTUAL_RETURN_DATE); + rental.setCarId(ID); + rental.setUserId(ID); + rental.setDeleted(false); + return rental; + } + + private RentalDto getRentalDto() { + RentalDto rentalDto = new RentalDto(); + rentalDto.setId(ID); + rentalDto.setRentalDate(RENTAL_DATE); + rentalDto.setActualReturnDate(ACTUAL_RETURN_DATE); + rentalDto.setReturnDate(RETURN_DATE); + rentalDto.setCarId(ID); + return rentalDto; + } + + private Car getCar() { + Car car = new Car(); + car.setId(ID); + car.setModel(MODEL); + car.setBrand(BRAND); + car.setType(UNIVERSAL_TYPE); + car.setInventory(INVENTORY); + car.setDailyFee(DAILY_FEE); + car.setDeleted(false); + return car; + } +} + diff --git a/src/test/java/com/marek/carsharing/service/user/UserServiceImplTest.java b/src/test/java/com/marek/carsharing/service/user/UserServiceImplTest.java new file mode 100644 index 0000000..19b6219 --- /dev/null +++ b/src/test/java/com/marek/carsharing/service/user/UserServiceImplTest.java @@ -0,0 +1,155 @@ +package com.marek.carsharing.service.user; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.marek.carsharing.dto.user.UpdateUserRequestDto; +import com.marek.carsharing.dto.user.UserDto; +import com.marek.carsharing.mapper.UserMapper; +import com.marek.carsharing.model.classes.User; +import com.marek.carsharing.model.enums.Role; +import com.marek.carsharing.repository.user.UserRepository; +import jakarta.persistence.EntityNotFoundException; +import java.util.Optional; +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; + +@ExtendWith(MockitoExtension.class) +class UserServiceImplTest { + public static final long ID = 1L; + public static final String EMAIL = "admin@simpleart.eu"; + public static final String FIRST_NAME = "B"; + public static final String LAST_NAME = "W"; + public static final String PASSWORD = "password"; + public static final Role ROLE = Role.MANAGER; + + @Mock + private UserRepository userRepository; + + @Mock + private UserMapper userMapper; + + @InjectMocks + private UserServiceImpl userService; + + @Test + @DisplayName("updateRole_withValidId_returnsUpdatedUserDto") + void updateRole_withValidId_returnsUpdatedUserDto() { + // Given + Long userId = 1L; + User user = getUser(); + UserDto userDto = getUserDto(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(userRepository.save(user)).thenReturn(user); + when(userMapper.toDto(user)).thenReturn(userDto); + + // When + UserDto result = userService.updateRole(userId); + + // Then + assertNotNull(result); + assertEquals(userDto, result); + verify(userRepository, times(1)).findById(userId); + verify(userRepository, times(1)).save(user); + verify(userMapper, times(1)).toDto(user); + verifyNoMoreInteractions(userRepository, userMapper); + } + + @Test + @DisplayName("updateRole_withInvalidId_throwsEntityNotFoundException") + void updateRole_withInvalidId_throwsEntityNotFoundException() { + // Given + + when(userRepository.findById(anyLong())).thenReturn(Optional.empty()); + + // When & Then + EntityNotFoundException entityNotFoundException = + assertThrows( + EntityNotFoundException.class, () -> userService.updateRole(anyLong())); + assertEquals("user with this id does not exist", + entityNotFoundException.getMessage()); + verify(userRepository, times(1)).findById(anyLong()); + verifyNoMoreInteractions(userRepository); + } + + @Test + @DisplayName("getMyProfile_withValidUser_returnsUserDto") + void getMyProfile_withValidUser_returnsUserDto() { + // Given + User user = getUser(); + UserDto userDto = getUserDto(); + + when(userMapper.toDto(user)).thenReturn(userDto); + + // When + UserDto result = userService.getMyProfile(user); + + // Then + assertNotNull(result); + assertEquals(userDto, result); + verify(userMapper, times(1)).toDto(user); + verifyNoMoreInteractions(userMapper); + } + + @Test + @DisplayName("updateMyProfile_withValidInput_returnsUpdatedUserDto") + void updateMyProfile_withValidInput_returnsUpdatedUserDto() { + // Given + User user = getUser(); + UpdateUserRequestDto requestDto = getUpdateUserRequestDto(); + UserDto userDto = getUserDto(); + + when(userRepository.save(user)).thenReturn(user); + when(userMapper.toDto(user)).thenReturn(userDto); + + // When + UserDto result = userService.updateMyProfile(user, requestDto); + + // Then + assertNotNull(result); + assertEquals(userDto, result); + verify(userRepository, times(1)).save(user); + verify(userMapper, times(1)).toDto(user); + verifyNoMoreInteractions(userRepository, userMapper); + } + + private UpdateUserRequestDto getUpdateUserRequestDto() { + return new UpdateUserRequestDto( + FIRST_NAME, LAST_NAME + ); + } + + private User getUser() { + User user = new User(); + user.setId(ID); + user.setEmail(EMAIL); + user.setFirstName(FIRST_NAME); + user.setLastName(LAST_NAME); + user.setPassword(PASSWORD); + user.setRole(ROLE); + user.setDeleted(false); + return user; + } + + private UserDto getUserDto() { + UserDto userDto = new UserDto(); + userDto.setId(ID); + userDto.setEmail(EMAIL); + userDto.setFirstName(FIRST_NAME); + userDto.setLastName(LAST_NAME); + userDto.setRole(String.valueOf(ROLE)); + return userDto; + } +} + diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..4d9b2de --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,15 @@ +spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password + +jwt.expiration=500000000 +jwt.secret=dfgdfhzvatggbchbfhgvxfgvfgvxcv74743828293 + +stripe.secret=sk_test_51Ppl3K04HmZD0PeB5oOkRtz\ + PxRmL9tMvWbc9ucS1Xoa1cf0REEYgnlFoR9Pw717jL5txghUOyLOp17DpaSM9G6ea00yYrGMmfT +domain=http://localhost:8080 + +telegram.bot.token=7409797315:AAGrN3mp85YrH2wvMH8XZ4EW0No6LibYMWs +telegram.bot.username=marekcarsharingBot +telegram.admin.chat.id=6469260399 diff --git a/src/test/resources/db/controller/add-to-cars.sql b/src/test/resources/db/controller/add-to-cars.sql new file mode 100644 index 0000000..fc3627f --- /dev/null +++ b/src/test/resources/db/controller/add-to-cars.sql @@ -0,0 +1,2 @@ +INSERT INTO cars (id, model, brand, type, inventory, daily_fee, deleted) +VALUES (1, 'model', 'brand', 'UNIVERSAL', 10, 10, false); diff --git a/src/test/resources/db/controller/add-to-cars2.sql b/src/test/resources/db/controller/add-to-cars2.sql new file mode 100644 index 0000000..846cd5c --- /dev/null +++ b/src/test/resources/db/controller/add-to-cars2.sql @@ -0,0 +1,2 @@ +INSERT INTO cars (id, model, brand, type, inventory, daily_fee, deleted) +VALUES (2, 'model', 'brand', 'UNIVERSAL', 10, 10, false); diff --git a/src/test/resources/db/controller/add-to-payments.sql b/src/test/resources/db/controller/add-to-payments.sql new file mode 100644 index 0000000..52f030e --- /dev/null +++ b/src/test/resources/db/controller/add-to-payments.sql @@ -0,0 +1,2 @@ +INSERT INTO payments (id, status, type, rental_id, session_url, session_id, amount_to_pay, deleted) +VALUES (1, 'PENDING', 'PAYMENT', 1, 'asfasnfkasf', '123124124', 100, false); diff --git a/src/test/resources/db/controller/add-to-rentals.sql b/src/test/resources/db/controller/add-to-rentals.sql new file mode 100644 index 0000000..9599d5c --- /dev/null +++ b/src/test/resources/db/controller/add-to-rentals.sql @@ -0,0 +1,2 @@ +INSERT INTO rentals (id, rental_date, return_date, actual_return_date, car_id, user_id, deleted) +VALUES (1, '2024-08-01', '2024-09-29', '2024-09-29', 1, 1, false); diff --git a/src/test/resources/db/controller/add-to-users.sql b/src/test/resources/db/controller/add-to-users.sql new file mode 100644 index 0000000..9e61e8b --- /dev/null +++ b/src/test/resources/db/controller/add-to-users.sql @@ -0,0 +1,2 @@ +INSERT INTO users (id, email, first_name, last_name, password, role, deleted) +VALUES(1, 'admin@simpleart.eu', 'Admin', 'User', 'password', 'MANAGER', false); diff --git a/src/test/resources/db/controller/delete-from-cars.sql b/src/test/resources/db/controller/delete-from-cars.sql new file mode 100644 index 0000000..fbb09fd --- /dev/null +++ b/src/test/resources/db/controller/delete-from-cars.sql @@ -0,0 +1 @@ +DELETE FROM cars WHERE id = 1; diff --git a/src/test/resources/db/controller/delete-from-cars2.sql b/src/test/resources/db/controller/delete-from-cars2.sql new file mode 100644 index 0000000..6ec79db --- /dev/null +++ b/src/test/resources/db/controller/delete-from-cars2.sql @@ -0,0 +1 @@ +DELETE FROM cars WHERE id = 2; diff --git a/src/test/resources/db/controller/delete-from-payments.sql b/src/test/resources/db/controller/delete-from-payments.sql new file mode 100644 index 0000000..3869c43 --- /dev/null +++ b/src/test/resources/db/controller/delete-from-payments.sql @@ -0,0 +1 @@ +DELETE FROM payments WHERE id = 1; diff --git a/src/test/resources/db/controller/delete-from-rentals.sql b/src/test/resources/db/controller/delete-from-rentals.sql new file mode 100644 index 0000000..7df1874 --- /dev/null +++ b/src/test/resources/db/controller/delete-from-rentals.sql @@ -0,0 +1 @@ +DELETE FROM rentals WHERE id = 1; \ No newline at end of file diff --git a/src/test/resources/db/controller/delete-from-users.sql b/src/test/resources/db/controller/delete-from-users.sql new file mode 100644 index 0000000..cad242d --- /dev/null +++ b/src/test/resources/db/controller/delete-from-users.sql @@ -0,0 +1 @@ +DELETE FROM users WHERE id = 1; \ No newline at end of file diff --git a/src/test/resources/db/repo/add-to-rental-repo.sql b/src/test/resources/db/repo/add-to-rental-repo.sql new file mode 100644 index 0000000..d94652c --- /dev/null +++ b/src/test/resources/db/repo/add-to-rental-repo.sql @@ -0,0 +1,2 @@ +INSERT INTO rentals (id, rental_date, return_date, actual_return_date, car_id, user_id, deleted) +VALUES (1, '2024-08-01', '2024-08-05', '2024-08-05', 1, 1, false); \ No newline at end of file diff --git a/src/test/resources/db/repo/add-to-user-repo.sql b/src/test/resources/db/repo/add-to-user-repo.sql new file mode 100644 index 0000000..a1e8613 --- /dev/null +++ b/src/test/resources/db/repo/add-to-user-repo.sql @@ -0,0 +1,2 @@ +INSERT INTO users (id, email, first_name, last_name, password, role, deleted) +VALUES (1, 'admin@simpleart.eu', 'B', 'C', 'password', 'MANAGER', false); diff --git a/src/test/resources/db/repo/clean-rental-repo.sql b/src/test/resources/db/repo/clean-rental-repo.sql new file mode 100644 index 0000000..0c149e4 --- /dev/null +++ b/src/test/resources/db/repo/clean-rental-repo.sql @@ -0,0 +1 @@ +DELETE FROM rentals; \ No newline at end of file diff --git a/src/test/resources/db/repo/clean-user-repo.sql b/src/test/resources/db/repo/clean-user-repo.sql new file mode 100644 index 0000000..d69513f --- /dev/null +++ b/src/test/resources/db/repo/clean-user-repo.sql @@ -0,0 +1 @@ +DELETE FROM users; \ No newline at end of file From bcc1f6806911ed63b8a0ab91184bbe08a0365f6c Mon Sep 17 00:00:00 2001 From: mgucma Date: Thu, 3 Oct 2024 09:19:05 +0200 Subject: [PATCH 2/5] app fix --- src/test/resources/db/repo/add-to-user-repo.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/resources/db/repo/add-to-user-repo.sql b/src/test/resources/db/repo/add-to-user-repo.sql index a1e8613..dcc9ab3 100644 --- a/src/test/resources/db/repo/add-to-user-repo.sql +++ b/src/test/resources/db/repo/add-to-user-repo.sql @@ -1,2 +1,4 @@ +DELETE FROM users; + INSERT INTO users (id, email, first_name, last_name, password, role, deleted) VALUES (1, 'admin@simpleart.eu', 'B', 'C', 'password', 'MANAGER', false); From 0ec98ba7e1211e790c6c82bf2a98c73a32b3bbe4 Mon Sep 17 00:00:00 2001 From: mgucma Date: Thu, 3 Oct 2024 09:25:04 +0200 Subject: [PATCH 3/5] app fix sql --- src/test/resources/db/repo/add-to-user-repo.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/resources/db/repo/add-to-user-repo.sql b/src/test/resources/db/repo/add-to-user-repo.sql index dcc9ab3..ca72f5c 100644 --- a/src/test/resources/db/repo/add-to-user-repo.sql +++ b/src/test/resources/db/repo/add-to-user-repo.sql @@ -1,4 +1,3 @@ DELETE FROM users; - INSERT INTO users (id, email, first_name, last_name, password, role, deleted) VALUES (1, 'admin@simpleart.eu', 'B', 'C', 'password', 'MANAGER', false); From 9aab12b3e0a96b5d1dd790741c4cc720fe00b4ef Mon Sep 17 00:00:00 2001 From: mgucma Date: Thu, 3 Oct 2024 09:27:44 +0200 Subject: [PATCH 4/5] app fix sql 2 --- src/test/resources/db/controller/add-to-users.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/resources/db/controller/add-to-users.sql b/src/test/resources/db/controller/add-to-users.sql index 9e61e8b..a1e8613 100644 --- a/src/test/resources/db/controller/add-to-users.sql +++ b/src/test/resources/db/controller/add-to-users.sql @@ -1,2 +1,2 @@ INSERT INTO users (id, email, first_name, last_name, password, role, deleted) -VALUES(1, 'admin@simpleart.eu', 'Admin', 'User', 'password', 'MANAGER', false); +VALUES (1, 'admin@simpleart.eu', 'B', 'C', 'password', 'MANAGER', false); From 73aa685c85e1ce4ca027d9ff9656ff1dcd7c5c0e Mon Sep 17 00:00:00 2001 From: mgucma Date: Thu, 3 Oct 2024 09:29:32 +0200 Subject: [PATCH 5/5] app fix sql 3 --- src/test/resources/db/controller/add-to-users.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/resources/db/controller/add-to-users.sql b/src/test/resources/db/controller/add-to-users.sql index a1e8613..ca72f5c 100644 --- a/src/test/resources/db/controller/add-to-users.sql +++ b/src/test/resources/db/controller/add-to-users.sql @@ -1,2 +1,3 @@ +DELETE FROM users; INSERT INTO users (id, email, first_name, last_name, password, role, deleted) VALUES (1, 'admin@simpleart.eu', 'B', 'C', 'password', 'MANAGER', false);