diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 416a758e..fc7feea0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,9 +36,10 @@ jobs: DB_PASSWORD: password SERVER_PORT: 8080 # Cloudinary dummy variables for testing - CLOUDINARY_CLOUD_NAME: dummy_cloud_name - CLOUDINARY_API_KEY: dummy_api_key - CLOUDINARY_API_SECRET: dummy_api_secret + CLOUDINARY_CLOUD_NAME: ${{ secrets.CLOUDINARY_CLOUD_NAME }} + CLOUDINARY_API_KEY: ${{ secrets.CLOUDINARY_API_KEY }} + CLOUDINARY_API_SECRET: ${{ secrets.CLOUDINARY_API_SECRET }} + # Email dummy variables for testing EMAIL: test@example.com EMAIL_PASSWORD: dummy_password diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5b23610e..6b166160 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,13 @@ on: jobs: test-docker: + + env: + # Cloudinary dummy variables for testing + CLOUDINARY_CLOUD_NAME: ${{ secrets.CLOUDINARY_CLOUD_NAME }} + CLOUDINARY_API_KEY: ${{ secrets.CLOUDINARY_API_KEY }} + CLOUDINARY_API_SECRET: ${{ secrets.CLOUDINARY_API_SECRET }} + # Use the latest Ubuntu runner for consistent test environment runs-on: ubuntu-latest @@ -36,4 +43,4 @@ jobs: if: always() # Run even if previous steps fail run: docker compose -f docker-compose-test.yml down -v # down: Stop and remove containers - # -v: Remove associated volumes \ No newline at end of file + # -v: Remove associated volumes diff --git a/docker-compose-test.yml b/docker-compose-test.yml index ec81d2f2..c3d4bade 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -30,6 +30,10 @@ services: - DB_URL_TEST=jdbc:mysql://sleepUp-test-db:3306/sleepUp # Database connection URL using service name - DB_USER=sleepUp # Database username - DB_PASSWORD=sleepUp123 # Database password + - CLOUDINARY_CLOUD_NAME=${CLOUDINARY_CLOUD_NAME} + - CLOUDINARY_API_KEY=${CLOUDINARY_API_KEY} + - CLOUDINARY_API_SECRET=${CLOUDINARY_API_SECRET} + # Restart policy - never restart (single-run test container) restart: no @@ -77,4 +81,4 @@ services: # Network configuration networks: sleepUp-network: - driver: bridge # Use bridge driver for container-to-container communication \ No newline at end of file + driver: bridge # Use bridge driver for container-to-container communication diff --git a/kubernetes/deployment.yml b/kubernetes/deployment.yml index 332b2d07..c4f9c7ba 100644 --- a/kubernetes/deployment.yml +++ b/kubernetes/deployment.yml @@ -38,7 +38,10 @@ spec: env: # Spring Boot profile configuration - name: SPRING_PROFILES_ACTIVE - value: "kubernetes" + valueFrom: + configMapKeyRef: + name: config + key: spring-profiles-active # Application server port - name: SERVER_PORT value: "8080" diff --git a/src/main/java/com/SleepUp/SU/accommodation/controller/AccommodationController.java b/src/main/java/com/SleepUp/SU/accommodation/controller/AccommodationController.java index ca68529f..f2669870 100644 --- a/src/main/java/com/SleepUp/SU/accommodation/controller/AccommodationController.java +++ b/src/main/java/com/SleepUp/SU/accommodation/controller/AccommodationController.java @@ -5,6 +5,9 @@ import com.SleepUp.SU.accommodation.dto.AccommodationResponseSummary; import com.SleepUp.SU.accommodation.service.AccommodationService; import com.SleepUp.SU.user.entity.CustomUserDetails; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -16,6 +19,7 @@ import java.util.List; +@Tag(name = "Accommodation", description = "Operation related to accommodations") @RestController @RequestMapping("/accommodations") @RequiredArgsConstructor @@ -23,12 +27,26 @@ public class AccommodationController { private final AccommodationService accommodationService; @GetMapping + @Operation(summary = "Get All Accommodations", description = "Retrieve a list of all accommodations with summary details.", + responses = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved list of accommodations"), + @ApiResponse(responseCode = "204", ref = "#/components/responses/NoContent"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) @ResponseStatus(HttpStatus.OK) public List getAllAccommodations(@AuthenticationPrincipal CustomUserDetails customUserDetails){ return accommodationService.getAllAccommodations(); } @GetMapping("/{id}") + @Operation(summary = "Get Accommodation Details", description = "Retrieve detailed information about a specific accommodation by its ID.", + responses = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved accommodation details"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "404", ref = "#/components/responses/AccommodationFound"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) @ResponseStatus(HttpStatus.OK) public AccommodationResponseDetail getAccommodationDetailById(@PathVariable Long id) { return accommodationService.getAccommodationById(id); @@ -36,6 +54,14 @@ public AccommodationResponseDetail getAccommodationDetailById(@PathVariable Long @PreAuthorize("isAuthenticated()") @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "Create Accommodation", description = "Create a new accommodation with the provided details.", + responses = { + @ApiResponse(responseCode = "201", description = "Successfully created a new accommodation"), + @ApiResponse(responseCode = "400", ref = "#/components/responses/BadRequest"), + @ApiResponse(responseCode = "401", ref = "#/components/responses/Unauthorized"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) @ResponseStatus(HttpStatus.CREATED) public AccommodationResponseDetail createAccommodation( @Valid @ModelAttribute AccommodationRequest accommodationRequest, @@ -45,6 +71,15 @@ public AccommodationResponseDetail createAccommodation( @PreAuthorize("hasRole('ADMIN') or @accommodationAccessEvaluator.isOwner(#id, principal.id)") @PutMapping(path = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "Update Accommodation", description = "Update the details of an existing accommodation by its ID.", + responses = { + @ApiResponse(responseCode = "200", description = "Successfully updated accommodation details"), + @ApiResponse(responseCode = "400", ref = "#/components/responses/BadRequest"), + @ApiResponse(responseCode = "401", ref = "#/components/responses/Unauthorized"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "404", ref = "#/components/responses/AccommodationFound"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) @ResponseStatus(HttpStatus.OK) public AccommodationResponseDetail updateAccommodation( @PathVariable Long id, @@ -54,6 +89,14 @@ public AccommodationResponseDetail updateAccommodation( @PreAuthorize("hasRole('ADMIN') or @accommodationAccessEvaluator.isOwner(#id, principal.id)") @DeleteMapping("/{id}") + @Operation(summary = "Delete Accommodation", description = "Delete an existing accommodation by its ID.", + responses = { + @ApiResponse(responseCode = "204", description = "Successfully deleted the accommodation"), + @ApiResponse(responseCode = "401", ref = "#/components/responses/Unauthorized"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "404", ref = "#/components/responses/AccommodationFound"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) @ResponseStatus(HttpStatus.NO_CONTENT) public ResponseEntity deleteAccommodation(@PathVariable Long id){ accommodationService.deleteAccommodation(id); diff --git a/src/main/java/com/SleepUp/SU/accommodation/dto/FilterAccommodationDTO.java b/src/main/java/com/SleepUp/SU/accommodation/dto/FilterAccommodationDTO.java index 84c1be13..3e0a5ad9 100644 --- a/src/main/java/com/SleepUp/SU/accommodation/dto/FilterAccommodationDTO.java +++ b/src/main/java/com/SleepUp/SU/accommodation/dto/FilterAccommodationDTO.java @@ -25,5 +25,7 @@ public record FilterAccommodationDTO( String location, LocalDate fromDate, - LocalDate toDate + LocalDate toDate, + + Boolean petFriendly ) {} diff --git a/src/main/java/com/SleepUp/SU/accommodation/filter/AccommodationFilterController.java b/src/main/java/com/SleepUp/SU/accommodation/filter/AccommodationFilterController.java index 4eb0c289..b961658d 100644 --- a/src/main/java/com/SleepUp/SU/accommodation/filter/AccommodationFilterController.java +++ b/src/main/java/com/SleepUp/SU/accommodation/filter/AccommodationFilterController.java @@ -2,6 +2,9 @@ import com.SleepUp.SU.accommodation.dto.AccommodationResponseSummary; import com.SleepUp.SU.accommodation.dto.FilterAccommodationDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -10,6 +13,7 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; +@Tag(name = "Accommodation Filter", description = "Operations related to filtering accommodations") @RestController @RequestMapping("/accommodations") @RequiredArgsConstructor @@ -18,6 +22,12 @@ public class AccommodationFilterController { private final AccommodationFilterService accommodationFilterService; @GetMapping("/filter") + @Operation(summary = "Filter Accommodations", description = "Retrieve a paginated list of accommodations based on filter criteria.", + responses = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved filtered accommodations"), + @ApiResponse(responseCode = "400", ref = "#/components/responses/BadRequest"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) @ResponseStatus(HttpStatus.OK) public Page getAllFilteredAccommodationsWithPagination( @ModelAttribute @Valid FilterAccommodationDTO filter, @@ -28,4 +38,4 @@ public Page getAllFilteredAccommodationsWithPagina return accommodationFilterService.getAllFilteredAccommodationsWithPagination(filter, pageable); } -} +} \ No newline at end of file diff --git a/src/main/java/com/SleepUp/SU/accommodation/filter/AccommodationSpecification.java b/src/main/java/com/SleepUp/SU/accommodation/filter/AccommodationSpecification.java index a2dc816a..eba0b1ee 100644 --- a/src/main/java/com/SleepUp/SU/accommodation/filter/AccommodationSpecification.java +++ b/src/main/java/com/SleepUp/SU/accommodation/filter/AccommodationSpecification.java @@ -62,30 +62,39 @@ public static Specification noBookingOverlap(LocalDate newStartDa return null; } + assert query != null; Subquery subquery = query.subquery(Long.class); Root reservationRoot = subquery.from(Reservation.class); subquery.select(cb.count(reservationRoot)); - // Match reservations for the same accommodation Predicate sameAccommodation = cb.equal(reservationRoot.get("accommodation").get("id"), root.get("id")); - // Exclude cancelled reservations Predicate notCancelled = cb.notEqual(reservationRoot.get("bookingStatus"), BookingStatus.CANCELLED); - // Overlapping date ranges Predicate overlap = cb.not(cb.or( cb.greaterThan(reservationRoot.get("checkInDate"), newEndDate), cb.lessThan(reservationRoot.get("checkOutDate"), newStartDate) )); - // Compose subquery where clause: same accommodation AND not cancelled AND overlapping subquery.where(cb.and(sameAccommodation, notCancelled, overlap)); - // Main predicate: count of overlapping, non-cancelled reservations must be zero return cb.equal(subquery, 0L); }; } + public static Specification petFriendly(Boolean petFriendly) { + return (root, query, cb) -> { + if (petFriendly == null) { + return null; + } + if (petFriendly) { + return cb.isTrue(root.get("petFriendly")); + } else { + return cb.isFalse(root.get("petFriendly")); + } + }; + } + public Specification buildSpecification(FilterAccommodationDTO filter) { return Specification.unrestricted() .and(AccommodationSpecification.hasName(filter.name())) @@ -94,6 +103,7 @@ public Specification buildSpecification(FilterAccommodationDTO fi .and(AccommodationSpecification.guestNumber(filter.guestNumber())) .and(AccommodationSpecification.locatedAt(filter.location())) .and(AccommodationSpecification.availableBetween(filter.fromDate(), filter.toDate())) - .and(noBookingOverlap(filter.fromDate(), filter.toDate())); + .and(noBookingOverlap(filter.fromDate(), filter.toDate())) + .and(AccommodationSpecification.petFriendly(filter.petFriendly())); } } diff --git a/src/main/java/com/SleepUp/SU/accommodation/owner/AccommodationOwnerController.java b/src/main/java/com/SleepUp/SU/accommodation/owner/AccommodationOwnerController.java index 91000a7f..5b7785be 100644 --- a/src/main/java/com/SleepUp/SU/accommodation/owner/AccommodationOwnerController.java +++ b/src/main/java/com/SleepUp/SU/accommodation/owner/AccommodationOwnerController.java @@ -2,6 +2,9 @@ import com.SleepUp.SU.accommodation.dto.AccommodationResponseSummary; import com.SleepUp.SU.user.entity.CustomUserDetails; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; @@ -13,6 +16,7 @@ import java.util.List; +@Tag(name = "Accommodation Owner", description = "Operations related to accommodations by owners") @RestController @RequestMapping("/accommodations") @RequiredArgsConstructor @@ -21,6 +25,13 @@ public class AccommodationOwnerController { private final AccommodationOwnerService accommodationOwnerService; @GetMapping("/me") + @Operation(summary = "Get All My Accommodations", description = "Retrieve a list of accommodations owned by the authenticated user.", + responses = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved list of accommodations"), + @ApiResponse(responseCode = "400", ref = "#/components/responses/BadRequest"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) @ResponseStatus(HttpStatus.OK) public List getAllOwnerAccommodations(@AuthenticationPrincipal CustomUserDetails customUserDetails){ return accommodationOwnerService.getAllAccommodationsByOwnerId(customUserDetails.getId()); diff --git a/src/main/java/com/SleepUp/SU/accommodation/security/AccommodationAccessEvaluator.java b/src/main/java/com/SleepUp/SU/accommodation/security/AccommodationAccessEvaluator.java index 0c258f92..59bb6421 100644 --- a/src/main/java/com/SleepUp/SU/accommodation/security/AccommodationAccessEvaluator.java +++ b/src/main/java/com/SleepUp/SU/accommodation/security/AccommodationAccessEvaluator.java @@ -1,6 +1,6 @@ package com.SleepUp.SU.accommodation.security; -import com.SleepUp.SU.accommodation.repository.AccommodationRepository; +import com.SleepUp.SU.accommodation.utils.AccommodationServiceHelper; import lombok.RequiredArgsConstructor; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Component; @@ -8,16 +8,15 @@ @Component @RequiredArgsConstructor public class AccommodationAccessEvaluator { - private final AccommodationRepository accommodationRepository; + private final AccommodationServiceHelper accommodationServiceHelper; public boolean isOwner(Long accommodationId, Long userId) { - boolean owner = accommodationRepository.existsByIdAndManagedBy_Id(accommodationId, userId); + boolean owner = accommodationServiceHelper.isAccommodationOwnedByUser(accommodationId, userId); if (!owner) { throw new AccessDeniedException( - "User ID " + userId + " cannot access Accommodation ID " + accommodationId + - ". Only the owner is authorized to access this resource." + "Cause: You are not the owner of this accommodation." ); } - return true; + return owner; } } diff --git a/src/main/java/com/SleepUp/SU/accommodation/utils/AccommodationServiceHelper.java b/src/main/java/com/SleepUp/SU/accommodation/utils/AccommodationServiceHelper.java index 2fad8f97..677ff5d3 100644 --- a/src/main/java/com/SleepUp/SU/accommodation/utils/AccommodationServiceHelper.java +++ b/src/main/java/com/SleepUp/SU/accommodation/utils/AccommodationServiceHelper.java @@ -28,6 +28,10 @@ public Accommodation getAccommodationEntityById(Long id) { return accommodationRepository.findById(id).orElseThrow(() -> new AccommodationNotFoundByIdException(id)); } + public boolean isAccommodationOwnedByUser(Long accommodationId, Long userId){ + return accommodationRepository.existsByIdAndManagedBy_Id(accommodationId, userId); + } + public void cloudinaryManagement(AccommodationRequest accommodationRequest, Accommodation accommodationIsExisting) { if (accommodationRequest.image() != null && !accommodationRequest.image().isEmpty()) { deleteImageCloudinary(accommodationIsExisting.getImageUrl()); diff --git a/src/main/java/com/SleepUp/SU/auth/AuthController.java b/src/main/java/com/SleepUp/SU/auth/AuthController.java index 27fcdda4..4d8afe1d 100644 --- a/src/main/java/com/SleepUp/SU/auth/AuthController.java +++ b/src/main/java/com/SleepUp/SU/auth/AuthController.java @@ -4,7 +4,7 @@ import com.SleepUp.SU.user.entity.CustomUserDetails; import com.SleepUp.SU.user.dto.UserRequest; import com.SleepUp.SU.user.dto.UserResponse; -import com.SleepUp.SU.utils.ApiMessageDto; +import com.SleepUp.SU.utils.dto.ApiMessageDto; import jakarta.mail.MessagingException; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; diff --git a/src/main/java/com/SleepUp/SU/auth/AuthService.java b/src/main/java/com/SleepUp/SU/auth/AuthService.java index d66fe320..bc6124e4 100644 --- a/src/main/java/com/SleepUp/SU/auth/AuthService.java +++ b/src/main/java/com/SleepUp/SU/auth/AuthService.java @@ -10,7 +10,7 @@ import com.SleepUp.SU.user.dto.UserRequest; import com.SleepUp.SU.user.dto.UserResponse; import com.SleepUp.SU.user.role.Role; -import com.SleepUp.SU.utils.ApiMessageDto; +import com.SleepUp.SU.utils.dto.ApiMessageDto; import com.SleepUp.SU.utils.email.EmailServiceHelper; import jakarta.mail.MessagingException; import jakarta.transaction.Transactional; @@ -42,11 +42,9 @@ public class AuthService { @Transactional public UserResponse register(UserRequest request) throws MessagingException { - userServiceHelper.validateUserDoesNotExist(request.username(), request.email()); - String encodedPassword = passwordEncoder.encode(request.password()); - User user = userMapper.toEntity(request, encodedPassword, Role.USER); - emailServiceHelper.sendWelcomeEmail(request, user); - return userMapper.toResponse(userRepository.save(user)); + User savedUser = userServiceHelper.createUser(request, Role.USER); + emailServiceHelper.sendWelcomeEmail(savedUser); + return userMapper.toResponse(savedUser); } public AuthResponse login(LoginRequest loginRequest) { diff --git a/src/main/java/com/SleepUp/SU/config/AppProperties.java b/src/main/java/com/SleepUp/SU/config/AppProperties.java deleted file mode 100644 index 956be949..00000000 --- a/src/main/java/com/SleepUp/SU/config/AppProperties.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.SleepUp.SU.config; - -import jakarta.validation.Valid; -import jakarta.validation.constraints.*; -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.validation.annotation.Validated; -import org.springframework.stereotype.Component; - -@Data -@Component -@ConfigurationProperties(prefix = "spring") -@Validated -public class AppProperties { - - @Valid - private DatasourceProperties datasource = new DatasourceProperties(); - - @NotNull(message = "server.port must not be null") - private Integer serverPort = 8080; - - @Valid - private MailProperties mail = new MailProperties(); - - @Valid - private JwtProperties jwt = new JwtProperties(); - - @NotBlank(message = "spring.application.name must not be blank") - private String applicationName = "SleepUpApp"; - - @Valid - private CloudinaryProperties cloudinary = new CloudinaryProperties(); - - @Data - @Validated - public static class DatasourceProperties { - @NotBlank(message = "spring.datasource.url must not be blank") - private String url = "jdbc:mysql://localhost:3306/testdb"; - - @NotBlank(message = "spring.datasource.username must not be blank") - private String username = "root"; - - @NotBlank(message = "spring.datasource.password must not be blank") - private String password = "password"; - } - - @Data - @Validated - public static class MailProperties { - @NotBlank(message = "spring.mail.username must not be blank") - private String username = "example@gmail.com"; - - @NotBlank(message = "spring.mail.password must not be blank") - private String password = "secret"; - - @NotBlank(message = "spring.mail.from must not be blank") - private String from = "example@gmail.com"; - } - - @Data - @Validated - public static class JwtProperties { - @NotBlank(message = "jwt.secret must not be blank") - private String secret = "default_jwt_secret"; - - @NotNull(message = "jwt.expiration-ms must not be null") - private Long expirationMs = 1800000L; - - @NotNull(message = "jwt.refresh-expiration-ms must not be null") - private Long refreshExpirationMs = 604800000L; - } - - @Data - @Validated - public static class CloudinaryProperties { - @NotBlank(message = "cloudinary.cloud_name must not be blank") - private String cloudName = "test_cloud"; - - @NotBlank(message = "cloudinary.api_key must not be blank") - private String apiKey = "test_key"; - - @NotBlank(message = "cloudinary.api_secret must not be blank") - private String apiSecret = "test_secret"; - } -} diff --git a/src/main/java/com/SleepUp/SU/config/CloudinaryConfig.java b/src/main/java/com/SleepUp/SU/config/CloudinaryConfig.java index e303d3a9..50f93d49 100644 --- a/src/main/java/com/SleepUp/SU/config/CloudinaryConfig.java +++ b/src/main/java/com/SleepUp/SU/config/CloudinaryConfig.java @@ -1,28 +1,25 @@ package com.SleepUp.SU.config; +import com.SleepUp.SU.config.properties.AppProperties; import com.cloudinary.Cloudinary; import com.cloudinary.utils.ObjectUtils; -import org.springframework.beans.factory.annotation.Value; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration +@RequiredArgsConstructor public class CloudinaryConfig { - @Value("${cloudinary.cloud_name:test_cloud}") - private String cloudName; - @Value("${cloudinary.api_key:test_key}") - private String apiKey; - - @Value("${cloudinary.api_secret:test_secret}") - private String apiSecret; + private final AppProperties appProperties; @Bean public Cloudinary cloudinary() { + AppProperties.CloudinaryProperties cp = appProperties.getCloudinary(); return new Cloudinary(ObjectUtils.asMap( - "cloud_name", cloudName, - "api_key", apiKey, - "api_secret", apiSecret + "cloud_name", cp.getCloudName(), + "api_key", cp.getApiKey(), + "api_secret", cp.getApiSecret() )); } } diff --git a/src/main/java/com/SleepUp/SU/config/StartupValidationRunner.java b/src/main/java/com/SleepUp/SU/config/StartupValidationRunner.java index a9c6650f..0d453e0a 100644 --- a/src/main/java/com/SleepUp/SU/config/StartupValidationRunner.java +++ b/src/main/java/com/SleepUp/SU/config/StartupValidationRunner.java @@ -1,11 +1,14 @@ package com.SleepUp.SU.config; +import com.SleepUp.SU.config.properties.DatasourceProperties; import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; import com.cloudinary.Cloudinary; -import com.cloudinary.utils.ObjectUtils; import javax.sql.DataSource; import java.sql.Connection; @@ -15,56 +18,56 @@ @RequiredArgsConstructor public class StartupValidationRunner implements ApplicationRunner { - private final AppProperties appProperties; + private static final Logger logger = LoggerFactory.getLogger(StartupValidationRunner.class); + private final DataSource dataSource; + private final DatasourceProperties datasourceProperties; + + @Autowired + private final Cloudinary cloudinary; @Override public void run(ApplicationArguments args) throws Exception { + logger.info("StartupValidationRunner started running"); validateConnections(); + logger.info("StartupValidationRunner completed validations successfully"); } private void validateConnections() { validateDatabaseConnection(); -// validateCloudinaryConnection(); + validateCloudinaryConnection(); } private void validateDatabaseConnection() { - if (appProperties.getDatasource() == null - || appProperties.getDatasource().getUrl() == null - || appProperties.getDatasource().getUrl().isEmpty()) { + logger.info("Validating database connection"); + + if (datasourceProperties.getUrl() == null + || datasourceProperties.getUrl().isEmpty()) { + logger.warn("DB URL is invalid or missing"); throw new RuntimeException("DB URL is invalid or missing"); } + try (Connection connection = dataSource.getConnection()) { if (connection == null || connection.isClosed()) { + logger.warn("Database connection not available"); throw new RuntimeException("Database connection not available"); + } else { + logger.info("Database connection is valid"); } } catch (SQLException e) { + logger.warn("Failed to connect to DB: {}", e.getMessage()); throw new RuntimeException("Failed to connect to DB: " + e.getMessage(), e); } } -// private void validateCloudinaryConnection() { -// AppProperties.CloudinaryProperties cloudinaryProps = appProperties.getCloudinary(); -// -// String cloudName = cloudinaryProps.getCloudName(); -// String apiKey = cloudinaryProps.getApiKey(); -// String apiSecret = cloudinaryProps.getApiSecret(); -// -// if (cloudName == null || apiKey == null || apiSecret == null -// || cloudName.isBlank() || apiKey.isBlank() || apiSecret.isBlank()) { -// throw new RuntimeException("Cloudinary credentials are missing or blank."); -// } -// -// Cloudinary cloudinaryInstance = new Cloudinary(ObjectUtils.asMap( -// "cloud_name", cloudName, -// "api_key", apiKey, -// "api_secret", apiSecret -// )); -// try { -// cloudinaryInstance.api().usage(ObjectUtils.emptyMap()); -// } catch (Exception e) { -// throw new RuntimeException("Cloudinary connection validation failed: " + e.getMessage(), e); -// } -// } - + public void validateCloudinaryConnection() { + logger.info("Validating Cloudinary connection"); + try { + cloudinary.api().usage(com.cloudinary.utils.ObjectUtils.emptyMap()); + logger.info("Cloudinary connection is valid"); + } catch (Exception e) { + logger.warn("Cloudinary connection validation failed: {}", e.getMessage()); + throw new RuntimeException("Cloudinary connection validation failed: " + e.getMessage(), e); + } + } } diff --git a/src/main/java/com/SleepUp/SU/config/SwaggerConfig.java b/src/main/java/com/SleepUp/SU/config/SwaggerConfig.java index 720dba37..0e327c20 100644 --- a/src/main/java/com/SleepUp/SU/config/SwaggerConfig.java +++ b/src/main/java/com/SleepUp/SU/config/SwaggerConfig.java @@ -1,10 +1,10 @@ package com.SleepUp.SU.config; -import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; -import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.security.SecurityRequirement; @@ -12,6 +12,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.util.Map; + @Configuration public class SwaggerConfig { @@ -33,8 +35,30 @@ public OpenAPI customOpenAPI() { .scheme("bearer") .bearerFormat("JWT")) .addSchemas("ErrorResponse", new Schema<>()) + + .addResponses("BadRequest", apiResponse(400, "Invalid input data")) + .addResponses("Unauthorized", apiResponse(401, "Authentication required or invalid token")) + .addResponses("Forbidden", apiResponse(403, "Access denied")) + .addResponses("AccommodationFound", apiResponse(404, "Accommodation not found")) + .addResponses("ReservationFound", apiResponse(404, "Reservation not found")) + .addResponses("UserFound", apiResponse(404, "User not found")) + .addResponses("Conflict", apiResponse(409, "Conflict with current state of the resource")) + .addResponses("InternalServerError", apiResponse(500, "Internal server error")) .addResponses("NoContent", new ApiResponse() .description("Successfully processed request with no content"))); } -} + + private ApiResponse apiResponse(int status, String message) { + return new ApiResponse() + .description(message) + .content(jsonError(status, message)); + } + + private Content jsonError(int status, String message) { + return new Content().addMediaType("application/json", + new MediaType() + .schema(new Schema<>().$ref("#/components/schemas/ErrorResponse")) + .example(Map.of("status", status, "message", message))); + } +} \ No newline at end of file diff --git a/src/main/java/com/SleepUp/SU/config/properties/AppProperties.java b/src/main/java/com/SleepUp/SU/config/properties/AppProperties.java new file mode 100644 index 00000000..cd65afc0 --- /dev/null +++ b/src/main/java/com/SleepUp/SU/config/properties/AppProperties.java @@ -0,0 +1,48 @@ +package com.SleepUp.SU.config.properties; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.*; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; +import org.springframework.stereotype.Component; + +@Component +@Getter +@Setter +@ConfigurationProperties +@Validated +public class AppProperties { + + @Valid + private JwtProperties jwt = new JwtProperties(); + + @Valid + private CloudinaryProperties cloudinary = new CloudinaryProperties(); + + @Data + public static class JwtProperties { + @NotBlank(message = "jwt.secret must not be blank") + private String secret; + + @NotNull(message = "jwt.expiration-ms must not be null") + private Long expirationMs; + + @NotNull(message = "jwt.refresh-expiration-ms must not be null") + private Long refreshExpirationMs; + } + + @Data + public static class CloudinaryProperties { + @NotBlank(message = "cloudinary.cloud_name must not be blank") + private String cloudName; + + @NotBlank(message = "cloudinary.api_key must not be blank") + private String apiKey; + + @NotBlank(message = "cloudinary.api_secret must not be blank") + private String apiSecret; + } +} diff --git a/src/main/java/com/SleepUp/SU/config/properties/DatasourceProperties.java b/src/main/java/com/SleepUp/SU/config/properties/DatasourceProperties.java new file mode 100644 index 00000000..a63e2871 --- /dev/null +++ b/src/main/java/com/SleepUp/SU/config/properties/DatasourceProperties.java @@ -0,0 +1,26 @@ +package com.SleepUp.SU.config.properties; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + + +@Component +@ConfigurationProperties(prefix = "spring.datasource") +@Validated +@Getter +@Setter +public class DatasourceProperties { + + @NotBlank(message = "spring.datasource.url must not be blank") + private String url; + + @NotBlank(message = "spring.datasource.username must not be blank") + private String username; + + @NotBlank(message = "spring.datasource.password must not be blank") + private String password; +} \ No newline at end of file diff --git a/src/main/java/com/SleepUp/SU/config/properties/MailProperties.java b/src/main/java/com/SleepUp/SU/config/properties/MailProperties.java new file mode 100644 index 00000000..c06aa556 --- /dev/null +++ b/src/main/java/com/SleepUp/SU/config/properties/MailProperties.java @@ -0,0 +1,29 @@ +package com.SleepUp.SU.config.properties; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +@Component +@ConfigurationProperties(prefix = "spring.mail") +@Validated +@Getter +@Setter +public class MailProperties { + + @NotBlank(message = "spring.mail.username must not be blank") + private String username; + + @NotBlank(message = "spring.mail.password must not be blank") + private String password; + + @NotBlank(message = "spring.mail.from must not be blank") + private String from; + + @NotNull(message = "spring.mail.port must not be null") + private Integer port; +} diff --git a/src/main/java/com/SleepUp/SU/exceptions/GlobalExceptionHandler.java b/src/main/java/com/SleepUp/SU/exceptions/GlobalExceptionHandler.java index 801f86e5..2fb71e51 100644 --- a/src/main/java/com/SleepUp/SU/exceptions/GlobalExceptionHandler.java +++ b/src/main/java/com/SleepUp/SU/exceptions/GlobalExceptionHandler.java @@ -111,7 +111,7 @@ public ResponseEntity handleEmptyOrMalformedBodyRequest(HttpMessa @ExceptionHandler(AccessDeniedException.class) public ResponseEntity handleAccessDeniedException(AccessDeniedException exception, HttpServletRequest request) { HttpStatus status = HttpStatus.FORBIDDEN; - ErrorResponse body = new ErrorResponse(status, "Forbidden: " + exception.getMessage(), request); + ErrorResponse body = new ErrorResponse(status, "Access Denied. You are not authorized to execute this action. " + exception.getMessage(), request); return ResponseEntity.status(status).body(body); } diff --git a/src/main/java/com/SleepUp/SU/reservation/accommodationOwner/ReservationOwnerController.java b/src/main/java/com/SleepUp/SU/reservation/accommodationOwner/ReservationOwnerController.java new file mode 100644 index 00000000..e27bfc58 --- /dev/null +++ b/src/main/java/com/SleepUp/SU/reservation/accommodationOwner/ReservationOwnerController.java @@ -0,0 +1,48 @@ +package com.SleepUp.SU.reservation.accommodationOwner; + +import com.SleepUp.SU.reservation.dto.ReservationAuthRequest; +import com.SleepUp.SU.reservation.dto.ReservationResponseDetail; +import com.SleepUp.SU.reservation.dto.ReservationResponseSummary; +import com.SleepUp.SU.user.entity.CustomUserDetails; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class ReservationOwnerController { + private final ReservationOwnerService reservationOwnerService; + + @PreAuthorize("hasRole('ADMIN') or @accommodationAccessEvaluator.isOwner(#id, principal.id)") + @GetMapping("/accommodations/{id}/reservations") + @ResponseStatus(HttpStatus.OK) + public List getAllReservationsForMyAccommodation(@PathVariable Long id){ + return reservationOwnerService.getReservationsForMyAccommodation(id); + } + + + @Operation(summary = "Update Reservation Status", description = "Update the status of an existing reservation.", + responses = { + @ApiResponse(responseCode = "200", description = "Successfully updated reservation status"), + @ApiResponse(responseCode = "400", ref = "#/components/responses/BadRequest"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "404", ref = "#/components/responses/ReservationFound"), + @ApiResponse(responseCode = "409", ref = "#/components/responses/Conflict"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) + @PreAuthorize("hasRole('ADMIN') or @reservationAccessEvaluator.isReservationAccommodationOwner(#id, principal.id)") + @PatchMapping("/reservations/{id}/status") + @ResponseStatus(HttpStatus.OK) + public ReservationResponseDetail updateReservationStatus(@AuthenticationPrincipal CustomUserDetails customUserDetails, + @PathVariable Long id, + @RequestBody ReservationAuthRequest reservationAuthRequest){ + return reservationOwnerService.updateStatus(id, reservationAuthRequest); + } + +} diff --git a/src/main/java/com/SleepUp/SU/reservation/owner/ReservationOwnerService.java b/src/main/java/com/SleepUp/SU/reservation/accommodationOwner/ReservationOwnerService.java similarity index 59% rename from src/main/java/com/SleepUp/SU/reservation/owner/ReservationOwnerService.java rename to src/main/java/com/SleepUp/SU/reservation/accommodationOwner/ReservationOwnerService.java index 6b951dd2..5a05eb6d 100644 --- a/src/main/java/com/SleepUp/SU/reservation/owner/ReservationOwnerService.java +++ b/src/main/java/com/SleepUp/SU/reservation/accommodationOwner/ReservationOwnerService.java @@ -1,16 +1,15 @@ -package com.SleepUp.SU.reservation.owner; +package com.SleepUp.SU.reservation.accommodationOwner; import com.SleepUp.SU.reservation.dto.ReservationAuthRequest; import com.SleepUp.SU.reservation.dto.ReservationResponseDetail; import com.SleepUp.SU.reservation.dto.ReservationResponseSummary; -import com.SleepUp.SU.user.entity.User; import java.util.List; public interface ReservationOwnerService { - List getAllReservationsOnMyAccommodation(User user, Long accommodationId); + + List getReservationsForMyAccommodation(Long accommodationId); ReservationResponseDetail updateStatus(Long id, ReservationAuthRequest reservationAuthRequest); - ReservationResponseDetail getReservationById(Long id); } diff --git a/src/main/java/com/SleepUp/SU/reservation/owner/ReservationOwnerServiceImpl.java b/src/main/java/com/SleepUp/SU/reservation/accommodationOwner/ReservationOwnerServiceImpl.java similarity index 66% rename from src/main/java/com/SleepUp/SU/reservation/owner/ReservationOwnerServiceImpl.java rename to src/main/java/com/SleepUp/SU/reservation/accommodationOwner/ReservationOwnerServiceImpl.java index 4f1ed271..de2af5a2 100644 --- a/src/main/java/com/SleepUp/SU/reservation/owner/ReservationOwnerServiceImpl.java +++ b/src/main/java/com/SleepUp/SU/reservation/accommodationOwner/ReservationOwnerServiceImpl.java @@ -1,20 +1,20 @@ -package com.SleepUp.SU.reservation.owner; +package com.SleepUp.SU.reservation.accommodationOwner; -import com.SleepUp.SU.reservation.entity.Reservation; -import com.SleepUp.SU.reservation.repository.ReservationRepository; -import com.SleepUp.SU.reservation.status.BookingStatus; -import com.SleepUp.SU.reservation.utils.ReservationServiceHelper; import com.SleepUp.SU.reservation.dto.ReservationAuthRequest; import com.SleepUp.SU.reservation.dto.ReservationMapper; import com.SleepUp.SU.reservation.dto.ReservationResponseDetail; import com.SleepUp.SU.reservation.dto.ReservationResponseSummary; -import com.SleepUp.SU.user.entity.User; +import com.SleepUp.SU.reservation.entity.Reservation; +import com.SleepUp.SU.reservation.repository.ReservationRepository; +import com.SleepUp.SU.reservation.status.BookingStatus; +import com.SleepUp.SU.reservation.utils.ReservationServiceHelper; import com.SleepUp.SU.utils.EntityUtil; import com.SleepUp.SU.utils.email.EmailServiceHelper; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.math.BigDecimal; import java.util.List; @Service @@ -22,17 +22,15 @@ public class ReservationOwnerServiceImpl implements ReservationOwnerService{ private final ReservationRepository reservationRepository; + private final EntityUtil entityUtil; private final ReservationMapper reservationMapper; private final ReservationServiceHelper reservationServiceHelper; - private final EntityUtil entityUtil; private final EmailServiceHelper emailServiceHelper; - @Override - public List getAllReservationsOnMyAccommodation(User user, Long id){ - List allReservationsByAccommodation = reservationRepository.findByAccommodationId(id); - if (allReservationsByAccommodation.isEmpty()){throw new RuntimeException("Empty list");} - return entityUtil.mapEntitiesToDTOs(allReservationsByAccommodation,reservationMapper::toSummary); + public List getReservationsForMyAccommodation(Long accommodationId) { + List reservations = reservationRepository.findByAccommodationId(accommodationId); + return entityUtil.mapEntitiesToDTOs(reservations, reservationMapper::toSummary); } @Override @@ -41,16 +39,11 @@ public ReservationResponseDetail updateStatus(Long id, ReservationAuthRequest re Reservation isExisting = reservationServiceHelper.getReservationEntityById(id); isExisting.setBookingStatus(reservationAuthRequest.bookingStatus()); - if (reservationAuthRequest.bookingStatus().equals(BookingStatus.CONFIRMED)){emailServiceHelper.sendReservationConfirmationEmail(isExisting.getUser(), isExisting.getAccommodation(), isExisting);} - if (reservationAuthRequest.bookingStatus().equals(BookingStatus.CANCELLED)){emailServiceHelper.sendCancellationByOwnerNotificationEmail(isExisting.getUser(), isExisting.getAccommodation(), isExisting);} + BigDecimal amount = isExisting.getTotalPrice(); - return reservationMapper.toDetail(isExisting); - } + if (reservationAuthRequest.bookingStatus().equals(BookingStatus.CONFIRMED)){emailServiceHelper.sendReservationConfirmationEmail(isExisting, amount);} + if (reservationAuthRequest.bookingStatus().equals(BookingStatus.CANCELLED)){emailServiceHelper.sendCancellationByOwnerNotificationEmail(isExisting);} - @Override - public ReservationResponseDetail getReservationById(Long id){ - Reservation isExisting = reservationServiceHelper.getReservationEntityById(id); return reservationMapper.toDetail(isExisting); } - } diff --git a/src/main/java/com/SleepUp/SU/reservation/admin/ReservationAdminController.java b/src/main/java/com/SleepUp/SU/reservation/admin/ReservationAdminController.java index 7e7e5694..2fcd226f 100644 --- a/src/main/java/com/SleepUp/SU/reservation/admin/ReservationAdminController.java +++ b/src/main/java/com/SleepUp/SU/reservation/admin/ReservationAdminController.java @@ -6,17 +6,16 @@ import lombok.RequiredArgsConstructor; import org.springframework.boot.autoconfigure.graphql.GraphQlProperties; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/reservations/admin") @RequiredArgsConstructor +@PreAuthorize("hasRole('ROLE_ADMIN')") public class ReservationAdminController { private final ReservationAdminServiceImpl reservationAdminService; @@ -26,4 +25,11 @@ public class ReservationAdminController { public List getAllReservations(@AuthenticationPrincipal Reservation reservation){ return reservationAdminService.getAllReservations(); } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteReservationByAdmin(@PathVariable Long id) { + reservationAdminService.deleteReservationByAdmin(id); + } + } diff --git a/src/main/java/com/SleepUp/SU/reservation/admin/ReservationAdminService.java b/src/main/java/com/SleepUp/SU/reservation/admin/ReservationAdminService.java index 0369045d..1e85c0b5 100644 --- a/src/main/java/com/SleepUp/SU/reservation/admin/ReservationAdminService.java +++ b/src/main/java/com/SleepUp/SU/reservation/admin/ReservationAdminService.java @@ -7,4 +7,6 @@ public interface ReservationAdminService { List getAllReservations(); + + void deleteReservationByAdmin(Long reservationId); } diff --git a/src/main/java/com/SleepUp/SU/reservation/admin/ReservationAdminServiceImpl.java b/src/main/java/com/SleepUp/SU/reservation/admin/ReservationAdminServiceImpl.java index 9348a568..5dbda1b0 100644 --- a/src/main/java/com/SleepUp/SU/reservation/admin/ReservationAdminServiceImpl.java +++ b/src/main/java/com/SleepUp/SU/reservation/admin/ReservationAdminServiceImpl.java @@ -4,6 +4,7 @@ import com.SleepUp.SU.reservation.dto.ReservationMapper; import com.SleepUp.SU.reservation.dto.ReservationResponseSummary; import com.SleepUp.SU.reservation.entity.Reservation; +import com.SleepUp.SU.reservation.exceptions.ReservationNotFoundByIdException; import com.SleepUp.SU.reservation.repository.ReservationRepository; import com.SleepUp.SU.reservation.utils.ReservationServiceHelper; import com.SleepUp.SU.utils.EntityUtil; @@ -26,6 +27,14 @@ public List getAllReservations(){ List reservations = reservationRepository.findAll(); return reservations.stream().map(reservationMapper::toSummary).toList(); } + + @Override + public void deleteReservationByAdmin(Long reservationId) { + if (!reservationRepository.existsById(reservationId)) { + throw new ReservationNotFoundByIdException(reservationId); + } + reservationRepository.deleteById(reservationId); + } } diff --git a/src/main/java/com/SleepUp/SU/reservation/controller/ReservationController.java b/src/main/java/com/SleepUp/SU/reservation/controller/ReservationController.java index a0bcd23a..d56817ac 100644 --- a/src/main/java/com/SleepUp/SU/reservation/controller/ReservationController.java +++ b/src/main/java/com/SleepUp/SU/reservation/controller/ReservationController.java @@ -1,22 +1,24 @@ package com.SleepUp.SU.reservation.controller; -import com.SleepUp.SU.reservation.dto.ApiMessage; import com.SleepUp.SU.reservation.service.ReservationService; -import com.SleepUp.SU.reservation.reservationtime.ReservationTime; +import com.SleepUp.SU.reservation.reservationTime.ReservationTime; import com.SleepUp.SU.reservation.dto.ReservationRequest; import com.SleepUp.SU.reservation.dto.ReservationResponseDetail; import com.SleepUp.SU.reservation.dto.ReservationResponseSummary; import com.SleepUp.SU.user.entity.CustomUserDetails; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; +@Tag(name = "Reservation", description = "Operations related to reservations") @RestController @RequestMapping("") @RequiredArgsConstructor @@ -25,6 +27,13 @@ public class ReservationController { private final ReservationService reservationService; @GetMapping("/reservations") + @Operation(summary = "Get My Reservations", description = "Retrieve a list of reservations made by the authenticated user.", + responses = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved list of reservations"), + @ApiResponse(responseCode = "400", ref = "#/components/responses/BadRequest"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) @ResponseStatus(HttpStatus.OK) public List getMyReservations( @AuthenticationPrincipal CustomUserDetails customUserDetails, @@ -34,6 +43,16 @@ public List getMyReservations( } @PostMapping("/accommodations/{accommodationId}/reservations") + @Operation(summary = "Create Reservation", description = "Create a new reservation for a specific accommodation.", + responses = { + @ApiResponse(responseCode = "201", description = "Successfully created a new reservation"), + @ApiResponse(responseCode = "400", ref = "#/components/responses/BadRequest"), + @ApiResponse(responseCode = "401", ref = "#/components/responses/Unauthorized"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "404", ref = "#/components/responses/ReservationFound"), + @ApiResponse(responseCode = "409", ref = "#/components/responses/Conflict"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) @ResponseStatus(HttpStatus.CREATED) public ReservationResponseDetail createReservation( @AuthenticationPrincipal CustomUserDetails customUserDetails, @@ -42,14 +61,4 @@ public ReservationResponseDetail createReservation( ) { return reservationService.createReservation(reservationRequest, customUserDetails.getUser(), accommodationId); } - - @PatchMapping(value = "/accommodations/{accommodationId}/reservations/cancel", produces = MediaType.APPLICATION_JSON_VALUE) - @ResponseStatus(HttpStatus.OK) - public ApiMessage cancelReservation( - @AuthenticationPrincipal CustomUserDetails customUserDetails, - @PathVariable Long accommodationId - ) { - return reservationService.cancelReservation(accommodationId); - } - } \ No newline at end of file diff --git a/src/main/java/com/SleepUp/SU/reservation/dto/ApiMessage.java b/src/main/java/com/SleepUp/SU/reservation/dto/ApiMessage.java deleted file mode 100644 index 684b8789..00000000 --- a/src/main/java/com/SleepUp/SU/reservation/dto/ApiMessage.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.SleepUp.SU.reservation.dto; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@AllArgsConstructor -@Getter -public class ApiMessage { - private final String message; -} \ No newline at end of file diff --git a/src/main/java/com/SleepUp/SU/reservation/dto/ReservationRequest.java b/src/main/java/com/SleepUp/SU/reservation/dto/ReservationRequest.java index ef390a29..ce6df858 100644 --- a/src/main/java/com/SleepUp/SU/reservation/dto/ReservationRequest.java +++ b/src/main/java/com/SleepUp/SU/reservation/dto/ReservationRequest.java @@ -13,11 +13,11 @@ public record ReservationRequest( Integer guestNumber, @NotNull - @JsonFormat(pattern = "yyyy-MM-dd") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") LocalDate checkInDate, @NotNull - @JsonFormat(pattern = "yyyy-MM-dd") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") LocalDate checkOutDate ) { } diff --git a/src/main/java/com/SleepUp/SU/reservation/dto/ReservationResponseDetail.java b/src/main/java/com/SleepUp/SU/reservation/dto/ReservationResponseDetail.java index 1b621fa4..f9f9111e 100644 --- a/src/main/java/com/SleepUp/SU/reservation/dto/ReservationResponseDetail.java +++ b/src/main/java/com/SleepUp/SU/reservation/dto/ReservationResponseDetail.java @@ -1,8 +1,10 @@ package com.SleepUp.SU.reservation.dto; import com.SleepUp.SU.reservation.status.BookingStatus; +import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Builder; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; @@ -12,10 +14,13 @@ public record ReservationResponseDetail( String userName, Integer guestNumber, String accommodationName, + @JsonFormat(pattern = "yyyy-MM-dd") LocalDate checkInDate, + @JsonFormat(pattern = "yyyy-MM-dd") LocalDate checkOutDate, BookingStatus bookingStatus, Boolean emailSent, - LocalDateTime createdDate + LocalDateTime createdDate, + BigDecimal totalPrice ) { } diff --git a/src/main/java/com/SleepUp/SU/reservation/dto/ReservationResponseSummary.java b/src/main/java/com/SleepUp/SU/reservation/dto/ReservationResponseSummary.java index 3c14ef24..acb6ad6d 100644 --- a/src/main/java/com/SleepUp/SU/reservation/dto/ReservationResponseSummary.java +++ b/src/main/java/com/SleepUp/SU/reservation/dto/ReservationResponseSummary.java @@ -1,8 +1,10 @@ package com.SleepUp.SU.reservation.dto; import com.SleepUp.SU.reservation.status.BookingStatus; +import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Builder; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; @@ -12,10 +14,11 @@ public record ReservationResponseSummary( String userName, Integer guestNumber, String accommodationName, + @JsonFormat(pattern = "yyyy-MM-dd") LocalDate checkInDate, + @JsonFormat(pattern = "yyyy-MM-dd") LocalDate checkOutDate, BookingStatus bookingStatus, - Boolean emailSent, - LocalDateTime createdDate + BigDecimal totalPrice ) { } diff --git a/src/main/java/com/SleepUp/SU/reservation/entity/Reservation.java b/src/main/java/com/SleepUp/SU/reservation/entity/Reservation.java index 56049a5f..7247c134 100644 --- a/src/main/java/com/SleepUp/SU/reservation/entity/Reservation.java +++ b/src/main/java/com/SleepUp/SU/reservation/entity/Reservation.java @@ -9,6 +9,7 @@ import lombok.*; import org.hibernate.annotations.CreationTimestamp; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; @@ -56,5 +57,8 @@ public class Reservation { @CreationTimestamp @Column(name = "created_date", nullable = false) private LocalDateTime createdDate; + + @Column(name = "total_price", nullable = false, precision = 19, scale = 2) + private BigDecimal totalPrice; } diff --git a/src/main/java/com/SleepUp/SU/reservation/owner/ReservationOwnerController.java b/src/main/java/com/SleepUp/SU/reservation/owner/ReservationOwnerController.java deleted file mode 100644 index b7e8bb5b..00000000 --- a/src/main/java/com/SleepUp/SU/reservation/owner/ReservationOwnerController.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.SleepUp.SU.reservation.owner; - -import com.SleepUp.SU.reservation.dto.ReservationAuthRequest; -import com.SleepUp.SU.reservation.dto.ReservationResponseDetail; -import com.SleepUp.SU.reservation.dto.ReservationResponseSummary; -import com.SleepUp.SU.user.entity.CustomUserDetails; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/reservations") -@RequiredArgsConstructor -public class ReservationOwnerController { - - private final ReservationOwnerService reservationOwnerService; - - @GetMapping("/accommodation/{id}") - @ResponseStatus(HttpStatus.OK) - public List getReservationOnMyAccommodation(@AuthenticationPrincipal CustomUserDetails customUserDetails, - @PathVariable Long id){ - return reservationOwnerService.getAllReservationsOnMyAccommodation(customUserDetails.getUser(), id); - } - - @PatchMapping("/{id}/status") - @ResponseStatus(HttpStatus.OK) - public ReservationResponseDetail updateReservationStatus(@AuthenticationPrincipal CustomUserDetails customUserDetails, - @PathVariable Long id, - @RequestBody ReservationAuthRequest reservationAuthRequest){ - return reservationOwnerService.updateStatus(id, reservationAuthRequest); - } - - @PostMapping("/{id}") - @ResponseStatus(HttpStatus.OK) - public ReservationResponseDetail getReservationById(@AuthenticationPrincipal CustomUserDetails customUserDetails, - @PathVariable Long id){ - return reservationOwnerService.getReservationById(id); - } -} diff --git a/src/main/java/com/SleepUp/SU/reservation/repository/ReservationRepository.java b/src/main/java/com/SleepUp/SU/reservation/repository/ReservationRepository.java index 6d5ce2d2..7fba4a68 100644 --- a/src/main/java/com/SleepUp/SU/reservation/repository/ReservationRepository.java +++ b/src/main/java/com/SleepUp/SU/reservation/repository/ReservationRepository.java @@ -9,6 +9,7 @@ import org.springframework.stereotype.Repository; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; @Repository @@ -62,6 +63,20 @@ List findOverlappingReservationsForAccommodation(@Param("accommodat @Param("checkOutDate") LocalDate checkOutDate, @Param("cancelledStatus") BookingStatus cancelledStatus); + /** + * Checks if a user has made a reservation for a specific accommodation + * in the last year (365 days) from the given date + * Cancelled reservations are excluded + */ + @Query("SELECT COUNT(r) > 0 FROM Reservation r WHERE r.user.id = :userId " + + "AND r.accommodation.id = :accommodationId " + + "AND r.bookingStatus != :cancelledStatus " + + "AND r.createdDate >= :oneYearAgo") + boolean existsReservationLessThanYear(@Param("userId") Long userId, + @Param("accommodationId") Long accommodationId, + @Param("oneYearAgo") LocalDateTime oneYearAgo, + @Param("cancelledStatus") BookingStatus cancelledStatus); + List findByUser_Id(Long userId); List findByUser_IdAndCheckInDateBefore(Long userId, LocalDate date); @@ -73,4 +88,5 @@ List findOverlappingReservationsForAccommodation(@Param("accommodat List findByCheckInDate(LocalDate date); boolean existsByIdAndUser_Id(Long reservationId, Long userId); + } \ No newline at end of file diff --git a/src/main/java/com/SleepUp/SU/reservation/reservationGuest/ReservationGuestController.java b/src/main/java/com/SleepUp/SU/reservation/reservationGuest/ReservationGuestController.java new file mode 100644 index 00000000..5f9b7b01 --- /dev/null +++ b/src/main/java/com/SleepUp/SU/reservation/reservationGuest/ReservationGuestController.java @@ -0,0 +1,76 @@ +package com.SleepUp.SU.reservation.reservationGuest; + +import com.SleepUp.SU.reservation.dto.ReservationRequest; +import com.SleepUp.SU.reservation.dto.ReservationResponseDetail; +import com.SleepUp.SU.user.entity.CustomUserDetails; +import com.SleepUp.SU.utils.dto.ApiMessageDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "Reservation Guest", description = "Operations related to reservations by guests") +@RestController +@RequestMapping("/reservations") +@RequiredArgsConstructor +public class ReservationGuestController { + + private final ReservationGuestService reservationGuestService; + + @Operation(summary = "Get Reservation by ID", description = "Retrieve detailed information about a specific reservation by its ID.", + responses = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved reservation details"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "404", ref = "#/components/responses/ReservationFound"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) + @GetMapping("/{id}") + @PreAuthorize("hasRole('ADMIN') or @reservationAccessEvaluator.isReservationGuestOrOwner(#id, principal.id)") + @ResponseStatus(HttpStatus.OK) + public ReservationResponseDetail getReservationById(@PathVariable Long id){ + return reservationGuestService.getReservationById(id); + } + + + @Operation(summary = "Update Reservation", description = "Update the details of an existing reservation.", + responses = { + @ApiResponse(responseCode = "200", description = "Successfully updated reservation"), + @ApiResponse(responseCode = "400", ref = "#/components/responses/BadRequest"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "404", ref = "#/components/responses/ReservationFound"), + @ApiResponse(responseCode = "409", ref = "#/components/responses/Conflict"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) + @PutMapping("/{id}") + @PreAuthorize("hasRole('ADMIN') or @reservationAccessEvaluator.isReservationGuest(#id, principal.id)") + @ResponseStatus(HttpStatus.OK) + public ApiMessageDto updateReservation(@AuthenticationPrincipal CustomUserDetails customUserDetails, + @PathVariable Long id, + @RequestBody ReservationRequest reservationRequest){ + return reservationGuestService.updateReservation(id, reservationRequest, customUserDetails.getUser()); + } + + @Operation(summary = "Cancel Reservation", description = "Cancel an existing reservation for a specific accommodation.", + responses = { + @ApiResponse(responseCode = "200", description = "Successfully canceled the reservation"), + @ApiResponse(responseCode = "400", ref = "#/components/responses/BadRequest"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "404", ref = "#/components/responses/ReservationFound"), + @ApiResponse(responseCode = "409", ref = "#/components/responses/Conflict"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) + @PatchMapping(value = "/{id}/cancel", produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasRole('ADMIN') or @reservationAccessEvaluator.isReservationGuest(#id, principal.id)") + @ResponseStatus(HttpStatus.OK) + public ApiMessageDto cancelReservation( + @PathVariable Long id + ) { + return reservationGuestService.cancelReservation(id); + } + +} \ No newline at end of file diff --git a/src/main/java/com/SleepUp/SU/reservation/reservationGuest/ReservationGuestService.java b/src/main/java/com/SleepUp/SU/reservation/reservationGuest/ReservationGuestService.java new file mode 100644 index 00000000..e2fd132d --- /dev/null +++ b/src/main/java/com/SleepUp/SU/reservation/reservationGuest/ReservationGuestService.java @@ -0,0 +1,16 @@ +package com.SleepUp.SU.reservation.reservationGuest; + +import com.SleepUp.SU.reservation.dto.*; +import com.SleepUp.SU.user.entity.User; +import com.SleepUp.SU.utils.dto.ApiMessageDto; + +import java.util.List; + +public interface ReservationGuestService { + + ApiMessageDto updateReservation(Long id, ReservationRequest reservationRequest, User user); + + ReservationResponseDetail getReservationById(Long id); + + ApiMessageDto cancelReservation(Long reservationId); +} diff --git a/src/main/java/com/SleepUp/SU/reservation/reservationGuest/ReservationGuestServiceImpl.java b/src/main/java/com/SleepUp/SU/reservation/reservationGuest/ReservationGuestServiceImpl.java new file mode 100644 index 00000000..d88527be --- /dev/null +++ b/src/main/java/com/SleepUp/SU/reservation/reservationGuest/ReservationGuestServiceImpl.java @@ -0,0 +1,76 @@ +package com.SleepUp.SU.reservation.reservationGuest; + +import com.SleepUp.SU.reservation.dto.*; +import com.SleepUp.SU.reservation.entity.Reservation; +import com.SleepUp.SU.reservation.repository.ReservationRepository; +import com.SleepUp.SU.reservation.service.ReservationService; +import com.SleepUp.SU.reservation.status.BookingStatus; +import com.SleepUp.SU.reservation.utils.ReservationServiceHelper; +import com.SleepUp.SU.user.entity.User; +import com.SleepUp.SU.utils.dto.ApiMessageDto; +import com.SleepUp.SU.utils.email.EmailServiceHelper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ReservationGuestServiceImpl implements ReservationGuestService { + + private final ReservationRepository reservationRepository; + private final ReservationMapper reservationMapper; + private final ReservationServiceHelper reservationServiceHelper; + private final EmailServiceHelper emailServiceHelper; + private final ReservationService reservationService; + + @Override + public ReservationResponseDetail getReservationById(Long id){ + Reservation isExisting = reservationServiceHelper.getReservationEntityById(id); + return reservationMapper.toDetail(isExisting); + } + + + + @Override + public ApiMessageDto updateReservation(Long id, ReservationRequest reservationRequest, User user) { + Reservation oldReservation = reservationServiceHelper.getReservationEntityById(id); + this.cancelReservation(id); + ReservationResponseDetail savedReservation = reservationService.createReservation( + reservationRequest, + user, + oldReservation.getAccommodation().getId() + ); + + String message = String.format( + "Your reservation in %s from %s to %s has been updated.\nOld dates were from %s to %s.", + savedReservation.accommodationName(), + savedReservation.checkInDate(), + savedReservation.checkOutDate(), + oldReservation.getCheckInDate(), + oldReservation.getCheckOutDate() + ); + + return new ApiMessageDto(message); + } + + @Override + public ApiMessageDto cancelReservation(Long reservationId) { + Reservation reservation = reservationServiceHelper.getReservationEntityById(reservationId); + + reservationServiceHelper.validateReservationCancellable(reservation); + reservation.setBookingStatus(BookingStatus.CANCELLED); + Reservation savedReservation = reservationRepository.save(reservation); + + emailServiceHelper.sendCancellationConfirmationEmail(reservation); + emailServiceHelper.sendCancellationNotificationToOwnerEmail(reservation); + + String message = String.format( + "Your reservation in %s from %s to %s has been cancelled", + savedReservation.getAccommodation().getName(), + savedReservation.getCheckInDate(), + savedReservation.getCheckOutDate() + ); + + return new ApiMessageDto(message); + } + +} diff --git a/src/main/java/com/SleepUp/SU/reservation/reservationtime/ReservationTime.java b/src/main/java/com/SleepUp/SU/reservation/reservationtime/ReservationTime.java index e25f9f99..736257e2 100644 --- a/src/main/java/com/SleepUp/SU/reservation/reservationtime/ReservationTime.java +++ b/src/main/java/com/SleepUp/SU/reservation/reservationtime/ReservationTime.java @@ -1,4 +1,4 @@ -package com.SleepUp.SU.reservation.reservationtime; +package com.SleepUp.SU.reservation.reservationTime; public enum ReservationTime { ALL, diff --git a/src/main/java/com/SleepUp/SU/reservation/security/ReservationAccessEvaluator.java b/src/main/java/com/SleepUp/SU/reservation/security/ReservationAccessEvaluator.java index 009e3412..20fc01d0 100644 --- a/src/main/java/com/SleepUp/SU/reservation/security/ReservationAccessEvaluator.java +++ b/src/main/java/com/SleepUp/SU/reservation/security/ReservationAccessEvaluator.java @@ -1,6 +1,8 @@ package com.SleepUp.SU.reservation.security; +import com.SleepUp.SU.accommodation.utils.AccommodationServiceHelper; import com.SleepUp.SU.reservation.repository.ReservationRepository; +import com.SleepUp.SU.reservation.utils.ReservationServiceHelper; import lombok.RequiredArgsConstructor; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Component; @@ -8,17 +10,41 @@ @Component @RequiredArgsConstructor public class ReservationAccessEvaluator { - private final ReservationRepository reservationRepository; + private final AccommodationServiceHelper accommodationServiceHelper; + private final ReservationServiceHelper reservationServiceHelper; public boolean isReservationGuest(Long reservationId, Long userId){ - boolean exists = reservationRepository.existsByIdAndUser_Id(reservationId, userId); + boolean exists = reservationServiceHelper.isReservationGuestTheUser(reservationId, userId); if (!exists) { throw new AccessDeniedException( - "User ID " + userId + " cannot access Reservation ID " + reservationId + - ". Only reservation guests can access this information." + "Cause: This reservation was not created by you." ); } - return true; + return exists; + } + + public boolean isReservationGuestOrOwner(Long reservationId, Long userId){ + Long accommodationId = reservationServiceHelper.getAccommodationIdFromReservationId(reservationId); + boolean exists = reservationServiceHelper.isReservationGuestTheUser(reservationId, userId) || accommodationServiceHelper.isAccommodationOwnedByUser(accommodationId, userId); + if (!exists) { + throw new AccessDeniedException( + "Cause: This reservation was not created by you or does not belong to any of your accommodations." + ); + + } + return exists; + } + + public boolean isReservationAccommodationOwner(Long reservationId, Long userId){ + Long accommodationId = reservationServiceHelper.getAccommodationIdFromReservationId(reservationId); + boolean exists = accommodationServiceHelper.isAccommodationOwnedByUser(accommodationId, userId); + if (!exists) { + throw new AccessDeniedException( + "Cause: This reservation does not belong to any of your accommodations." + ); + + } + return exists; } } diff --git a/src/main/java/com/SleepUp/SU/reservation/service/ReservationService.java b/src/main/java/com/SleepUp/SU/reservation/service/ReservationService.java index b392b4cf..046f6178 100644 --- a/src/main/java/com/SleepUp/SU/reservation/service/ReservationService.java +++ b/src/main/java/com/SleepUp/SU/reservation/service/ReservationService.java @@ -1,10 +1,9 @@ package com.SleepUp.SU.reservation.service; -import com.SleepUp.SU.reservation.dto.ApiMessage; import com.SleepUp.SU.reservation.dto.ReservationRequest; import com.SleepUp.SU.reservation.dto.ReservationResponseDetail; import com.SleepUp.SU.reservation.dto.ReservationResponseSummary; -import com.SleepUp.SU.reservation.reservationtime.ReservationTime; +import com.SleepUp.SU.reservation.reservationTime.ReservationTime; import com.SleepUp.SU.user.entity.User; import java.util.List; @@ -14,6 +13,4 @@ public interface ReservationService { List getMyReservations(Long userId, ReservationTime time); ReservationResponseDetail createReservation(ReservationRequest reservationRequest, User user, Long accommodationId); - - ApiMessage cancelReservation(Long reservationId); } diff --git a/src/main/java/com/SleepUp/SU/reservation/service/ReservationServiceImpl.java b/src/main/java/com/SleepUp/SU/reservation/service/ReservationServiceImpl.java index daece386..2271b45f 100644 --- a/src/main/java/com/SleepUp/SU/reservation/service/ReservationServiceImpl.java +++ b/src/main/java/com/SleepUp/SU/reservation/service/ReservationServiceImpl.java @@ -6,17 +6,18 @@ import com.SleepUp.SU.reservation.entity.Reservation; import com.SleepUp.SU.reservation.exceptions.ReservationAccommodationOwnerException; import com.SleepUp.SU.reservation.repository.ReservationRepository; -import com.SleepUp.SU.reservation.reservationtime.ReservationTime; +import com.SleepUp.SU.reservation.reservationTime.ReservationTime; import com.SleepUp.SU.reservation.status.BookingStatus; import com.SleepUp.SU.reservation.utils.ReservationServiceHelper; import com.SleepUp.SU.user.entity.User; import com.SleepUp.SU.utils.email.EmailServiceHelper; import com.SleepUp.SU.utils.EntityUtil; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import java.math.BigDecimal; import java.time.LocalDate; +import java.time.temporal.ChronoUnit; import java.util.List; @Service @@ -63,30 +64,14 @@ public ReservationResponseDetail createReservation(ReservationRequest reservatio user, accommodation, false); + boolean discount = reservationServiceHelper.validateReservationAccommodationLessThanOneYear(accommodationId, user.getId()); + BigDecimal amount = reservationServiceHelper.calculateReservationPrice(reservationRequest, accommodation, discount); + + newReservation.setTotalPrice(amount); Reservation savedReservation = reservationRepository.save(newReservation); - emailServiceHelper.sendOwnerReservedNotification(user, accommodation, savedReservation); + emailServiceHelper.sendOwnerReservedNotification(savedReservation); return reservationMapper.toDetail(savedReservation); } - @Override - public ApiMessage cancelReservation(Long reservationId) { - Reservation reservation = reservationServiceHelper.getReservationEntityById(reservationId); - - reservationServiceHelper.validateReservationCancellable(reservation); - reservation.setBookingStatus(BookingStatus.CANCELLED); - Reservation savedReservation = reservationRepository.save(reservation); - - emailServiceHelper.sendCancellationConfirmationEmail(reservation.getUser(), reservation.getAccommodation(), reservation); - emailServiceHelper.sendCancellationNotificationToOwnerEmail(reservation.getUser(), reservation.getAccommodation(), reservation); - - String message = String.format( - "Your reservation in %s from %s to %s has been cancelled", - savedReservation.getAccommodation().getName(), - savedReservation.getCheckInDate(), - savedReservation.getCheckOutDate() - ); - - return new ApiMessage(message); - } } \ No newline at end of file diff --git a/src/main/java/com/SleepUp/SU/reservation/utils/ReservationServiceHelper.java b/src/main/java/com/SleepUp/SU/reservation/utils/ReservationServiceHelper.java index de78c60f..05d09e1c 100644 --- a/src/main/java/com/SleepUp/SU/reservation/utils/ReservationServiceHelper.java +++ b/src/main/java/com/SleepUp/SU/reservation/utils/ReservationServiceHelper.java @@ -8,11 +8,16 @@ import com.SleepUp.SU.reservation.dto.ReservationRequest; import com.SleepUp.SU.reservation.exceptions.*; import com.SleepUp.SU.reservation.status.BookingStatus; +import com.SleepUp.SU.user.entity.User; import com.SleepUp.SU.utils.EntityUtil; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.List; @Service @@ -25,6 +30,30 @@ public Reservation getReservationEntityById(Long id){ return reservationRepository.findById(id).orElseThrow(() -> new ReservationNotFoundByIdException(id)); } + public boolean isReservationGuestTheUser(Long reservationId, Long userId){ + return reservationRepository.existsByIdAndUser_Id(reservationId, userId); + } + + public Long getAccommodationIdFromReservationId(Long reservationId){ + return getReservationEntityById(reservationId).getAccommodation().getId(); + } + + public BigDecimal calculateReservationPrice(ReservationRequest reservationRequest, Accommodation accommodation, boolean discount) { + long days = ChronoUnit.DAYS.between(reservationRequest.checkInDate(), reservationRequest.checkOutDate()); + + BigDecimal pricePerDay = BigDecimal.valueOf(accommodation.getPrice()); + BigDecimal totalAmount = pricePerDay.multiply(BigDecimal.valueOf(days)); + + if (discount) { + BigDecimal discountMultiplier = BigDecimal.valueOf(0.8); + totalAmount = totalAmount.multiply(discountMultiplier); + } + + totalAmount = totalAmount.setScale(2, RoundingMode.HALF_UP); + + return totalAmount; + } + public void validateReservationDates(ReservationRequest reservationRequest) { EntityUtil.validateCheckInOutDates(reservationRequest.checkInDate(), reservationRequest.checkOutDate()); } @@ -121,4 +150,15 @@ public void validateReservationCancellable(Reservation reservation) { throw new ReservationModificationException("Cannot modify a reservation that has already started"); } } + + public boolean validateReservationAccommodationLessThanOneYear(Long accommodationId, Long userId){ + + LocalDateTime oneYearAgo = LocalDate.now().minusYears(1).atStartOfDay(); + + return reservationRepository.existsReservationLessThanYear( + userId, + accommodationId, + oneYearAgo, + BookingStatus.CANCELLED); + } } \ No newline at end of file diff --git a/src/main/java/com/SleepUp/SU/security/CustomAccessDeniedHandler.java b/src/main/java/com/SleepUp/SU/security/CustomAccessDeniedHandler.java index b78eddc4..d23c6e0d 100644 --- a/src/main/java/com/SleepUp/SU/security/CustomAccessDeniedHandler.java +++ b/src/main/java/com/SleepUp/SU/security/CustomAccessDeniedHandler.java @@ -33,7 +33,7 @@ public void handle(HttpServletRequest request, HttpServletResponse response, ErrorResponse errorResponse = new ErrorResponse( HttpStatus.FORBIDDEN, - "Access Denied. " + accessDeniedException.getMessage(), + "Access Denied. You are not authorized to execute this action." + accessDeniedException.getMessage(), request ); diff --git a/src/main/java/com/SleepUp/SU/security/config/SecurityConfig.java b/src/main/java/com/SleepUp/SU/security/config/SecurityConfig.java index f9c0b26d..0a32ffed 100644 --- a/src/main/java/com/SleepUp/SU/security/config/SecurityConfig.java +++ b/src/main/java/com/SleepUp/SU/security/config/SecurityConfig.java @@ -75,12 +75,15 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthFilter .requestMatchers(HttpMethod.DELETE, ApiPrefixHelper.prefixPaths("/users/me")).authenticated() .requestMatchers(HttpMethod.GET, ApiPrefixHelper.prefixPaths("/users/admin/**")).hasRole("ADMIN") + .requestMatchers(HttpMethod.POST, ApiPrefixHelper.prefixPaths("/users/admin/**")).hasRole("ADMIN") .requestMatchers(HttpMethod.PUT, ApiPrefixHelper.prefixPaths("/users/admin/**")).hasRole("ADMIN") + .requestMatchers(HttpMethod.DELETE, ApiPrefixHelper.prefixPaths("/users/admin/**")).hasRole("ADMIN") .requestMatchers(HttpMethod.GET, ApiPrefixHelper.prefixPaths("/reservations/admin")).hasRole("ADMIN") .requestMatchers(HttpMethod.GET, ApiPrefixHelper.prefixPaths("/accommodations")).permitAll() .requestMatchers(HttpMethod.GET, "/accommodations").permitAll() + .requestMatchers(HttpMethod.GET, "/accommodations/**").permitAll() .requestMatchers(HttpMethod.GET, ApiPrefixHelper.prefixPaths("/accommodations/filter**")).permitAll() .requestMatchers(HttpMethod.GET, "/accommodations/filter**").permitAll() diff --git a/src/main/java/com/SleepUp/SU/security/dto/TokenPairAndExpiration.java b/src/main/java/com/SleepUp/SU/security/dto/TokenPairAndExpiration.java deleted file mode 100644 index 806ade0f..00000000 --- a/src/main/java/com/SleepUp/SU/security/dto/TokenPairAndExpiration.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.SleepUp.SU.security.dto; - -public record TokenPairAndExpiration(String accessToken, String refreshToken, long jwtExpirationMs) {} - diff --git a/src/main/java/com/SleepUp/SU/security/jwt/JwtAuthFilter.java b/src/main/java/com/SleepUp/SU/security/jwt/JwtAuthFilter.java index 4f26b280..4df1b130 100644 --- a/src/main/java/com/SleepUp/SU/security/jwt/JwtAuthFilter.java +++ b/src/main/java/com/SleepUp/SU/security/jwt/JwtAuthFilter.java @@ -76,7 +76,6 @@ protected void doFilterInternal(HttpServletRequest request, @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { - // Skip the filter for excluded paths with POST method String path = request.getRequestURI(); return EXCLUDED_PATHS.contains(path) && "POST".equalsIgnoreCase(request.getMethod()); } diff --git a/src/main/java/com/SleepUp/SU/security/jwt/JwtService.java b/src/main/java/com/SleepUp/SU/security/jwt/JwtService.java index 4e622517..c7e1d994 100644 --- a/src/main/java/com/SleepUp/SU/security/jwt/JwtService.java +++ b/src/main/java/com/SleepUp/SU/security/jwt/JwtService.java @@ -13,7 +13,9 @@ import javax.crypto.SecretKey; import java.time.Instant; import java.util.Date; +import java.util.HashMap; import java.util.Map; +import java.util.UUID; import java.util.function.Function; @Service @@ -24,7 +26,7 @@ public class JwtService { private final SecretKey secretKey; private final long jwtExpirationMs; - private final long jwtRefreshExpirationMs = 7 * 24 * 60 * 60 * 1000; // Example: 7 days in milliseconds + private final long jwtRefreshExpirationMs = 7 * 24 * 60 * 60 * 1000; public JwtService( @Value("${jwt.secret}") String secret, @@ -34,23 +36,29 @@ public JwtService( } public String generateRefreshToken(UserDetails userDetails) { - Map claims = Map.of( - ROLE_CLAIM, userDetails.getAuthorities().stream() - .map(GrantedAuthority::getAuthority) - .toList() - ); + Map claims = new HashMap<>(); + claims.put(ROLE_CLAIM, userDetails.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .toList()); + claims.put("jti", UUID.randomUUID().toString()); + claims.put("iat", System.currentTimeMillis()); + return buildToken(claims, userDetails.getUsername(), jwtRefreshExpirationMs); } + public String generateAccessToken(UserDetails userDetails) { - Map claims = Map.of( - ROLE_CLAIM, userDetails.getAuthorities().stream() - .map(GrantedAuthority::getAuthority) - .toList() - ); + Map claims = new HashMap<>(); + claims.put(ROLE_CLAIM, userDetails.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .toList()); + claims.put("jti", UUID.randomUUID().toString()); + claims.put("iat", System.currentTimeMillis()); + return buildToken(claims, userDetails.getUsername(), jwtExpirationMs); } + private String buildToken(Map claims, String subject, long expirationMs) { Instant now = Instant.now(); Instant expiry = now.plusMillis(expirationMs); @@ -85,7 +93,7 @@ private Claims extractAllClaims(String token) { .getPayload(); } - private boolean isTokenExpired(String token) { + public boolean isTokenExpired(String token) { return extractExpiration(token).before(Date.from(Instant.now())); } diff --git a/src/main/java/com/SleepUp/SU/user/admin/UserAdminController.java b/src/main/java/com/SleepUp/SU/user/admin/UserAdminController.java index 0946f22b..2335acf7 100644 --- a/src/main/java/com/SleepUp/SU/user/admin/UserAdminController.java +++ b/src/main/java/com/SleepUp/SU/user/admin/UserAdminController.java @@ -1,7 +1,13 @@ package com.SleepUp.SU.user.admin; +import com.SleepUp.SU.user.dto.UserRequest; import com.SleepUp.SU.user.dto.UserRequestAdmin; import com.SleepUp.SU.user.dto.UserResponse; +import com.SleepUp.SU.user.role.Role; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -10,8 +16,9 @@ import java.util.List; +@Tag(name = "User Admin", description = "Operations related to user management by admin") @RestController -@RequestMapping("/api/users/admin") +@RequestMapping("/users/admin") @RequiredArgsConstructor @PreAuthorize("hasRole('ADMIN')") public class UserAdminController { @@ -19,32 +26,69 @@ public class UserAdminController { private final UserAdminService userAdminService; @GetMapping + @Operation(summary = "List All Users", description = "Retrieve a list of all users in the system.", + responses = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved list of users"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) public List listAllUsers() { return userAdminService.getAllUsers(); } @GetMapping("/{id}") + @Operation(summary = "Get User by ID", description = "Retrieve detailed information about a specific user by their ID.", + responses = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved user details"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "404", ref = "#/components/responses/UserFound"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) public ResponseEntity getUserById(@PathVariable Long id) { UserResponse userResponse = userAdminService.getUserById(id); return ResponseEntity.ok(userResponse); } @PostMapping - public ResponseEntity createUser(@RequestBody UserRequestAdmin userRequestAdmin) { - UserResponse newUser = userAdminService.createUser(userRequestAdmin); + @Operation(summary = "Create New User", description = "Create a new user with the provided details.", + responses = { + @ApiResponse(responseCode = "201", description = "Successfully created a new user"), + @ApiResponse(responseCode = "400", ref = "#/components/responses/BadRequest"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "409", ref = "#/components/responses/Conflict"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) + public ResponseEntity createUser(@Valid @RequestBody UserRequest userRequest, + @RequestParam Role role) { + UserResponse newUser = userAdminService.createUser(userRequest, role); return ResponseEntity.status(HttpStatus.CREATED).body(newUser); } @PutMapping("/{id}") + @Operation(summary = "Update User", description = "Update the details of an existing user.", + responses = { + @ApiResponse(responseCode = "200", description = "Successfully updated user details"), + @ApiResponse(responseCode = "400", ref = "#/components/responses/BadRequest"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "404", ref = "#/components/responses/UserFound"), + @ApiResponse(responseCode = "409", ref = "#/components/responses/Conflict"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) public ResponseEntity updateUser(@PathVariable Long id, @RequestBody UserRequestAdmin userRequestAdmin) { UserResponse updatedUser = userAdminService.updateUser(id, userRequestAdmin); return ResponseEntity.ok(updatedUser); } @DeleteMapping("/{id}") + @Operation(summary = "Delete User", description = "Delete a user by their ID.", + responses = { + @ApiResponse(responseCode = "204", description = "Successfully deleted the user"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "404", ref = "#/components/responses/UserFound"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) public ResponseEntity deleteUser(@PathVariable Long id) { userAdminService.deleteUserById(id); return ResponseEntity.noContent().build(); } -} - +} \ No newline at end of file diff --git a/src/main/java/com/SleepUp/SU/user/admin/UserAdminService.java b/src/main/java/com/SleepUp/SU/user/admin/UserAdminService.java index 9da77b91..6cffdcd1 100644 --- a/src/main/java/com/SleepUp/SU/user/admin/UserAdminService.java +++ b/src/main/java/com/SleepUp/SU/user/admin/UserAdminService.java @@ -1,7 +1,9 @@ package com.SleepUp.SU.user.admin; +import com.SleepUp.SU.user.dto.UserRequest; import com.SleepUp.SU.user.dto.UserRequestAdmin; import com.SleepUp.SU.user.dto.UserResponse; +import com.SleepUp.SU.user.role.Role; import org.springframework.security.core.userdetails.UserDetailsService; import java.util.List; @@ -12,7 +14,7 @@ public interface UserAdminService extends UserDetailsService { UserResponse getUserById(Long userId); - UserResponse createUser(UserRequestAdmin userRequestAdmin); + UserResponse createUser(UserRequest userRequest, Role role); UserResponse updateUser(Long userId, UserRequestAdmin userRequestAdmin); diff --git a/src/main/java/com/SleepUp/SU/user/admin/UserAdminServiceImpl.java b/src/main/java/com/SleepUp/SU/user/admin/UserAdminServiceImpl.java index ca5bba49..e541212d 100644 --- a/src/main/java/com/SleepUp/SU/user/admin/UserAdminServiceImpl.java +++ b/src/main/java/com/SleepUp/SU/user/admin/UserAdminServiceImpl.java @@ -3,18 +3,22 @@ import com.SleepUp.SU.accommodation.repository.AccommodationRepository; import com.SleepUp.SU.reservation.entity.Reservation; import com.SleepUp.SU.reservation.repository.ReservationRepository; +import com.SleepUp.SU.reservation.status.BookingStatus; +import com.SleepUp.SU.user.dto.UserRequest; import com.SleepUp.SU.user.entity.CustomUserDetails; import com.SleepUp.SU.user.entity.User; import com.SleepUp.SU.user.repository.UserRepository; import com.SleepUp.SU.user.dto.UserRequestAdmin; +import com.SleepUp.SU.user.role.Role; import com.SleepUp.SU.user.utils.UserServiceHelper; import com.SleepUp.SU.user.dto.UserMapper; import com.SleepUp.SU.user.dto.UserResponse; import com.SleepUp.SU.utils.EntityUtil; +import com.SleepUp.SU.utils.email.EmailServiceHelper; +import com.SleepUp.SU.utils.exceptions.UserNotFoundByIdException; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -28,9 +32,9 @@ public class UserAdminServiceImpl implements UserAdminService { private final UserMapper userMapper; private final EntityUtil mapperUtil; private final UserServiceHelper userServiceHelper; - private final PasswordEncoder passwordEncoder; private final AccommodationRepository accommodationRepository; private final ReservationRepository reservationRepository; + private final EmailServiceHelper emailServiceHelper; @Override public List getAllUsers() { @@ -39,32 +43,21 @@ public List getAllUsers() { @Override public UserResponse getUserById(Long userId) { - return userMapper.toResponse(userServiceHelper.findById(userId)); + return userMapper.toResponse(userServiceHelper.getUserEntityById(userId)); } @Override - public UserResponse createUser(UserRequestAdmin userRequestAdmin) { - if (userRepository.findByUsername(userRequestAdmin.username()).isPresent()) { - throw new RuntimeException("Username already exists " + userRequestAdmin.username()); - } - - String encodedPassword = passwordEncoder.encode(userRequestAdmin.password()); - User user = userMapper.toEntityAdmin(userRequestAdmin, encodedPassword); - User savedUser = userRepository.save(user); + public UserResponse createUser(UserRequest userRequest, Role role) { + User savedUser = userServiceHelper.createUser(userRequest, role); + emailServiceHelper.sendWelcomeEmail(savedUser); return userMapper.toResponse(savedUser); } @Override @Transactional - public UserResponse updateUser(Long userId, UserRequestAdmin userRequestAdmin) { - User existingUser = userRepository.findById(userId) - .orElseThrow(() -> new RuntimeException("User not found with id: " + userId)); - - userServiceHelper.updateUserDataAdmin(userRequestAdmin, existingUser); - - existingUser.setRole(userRequestAdmin.role()); - - User updatedUser = userRepository.save(existingUser); + public UserResponse updateUser(Long userId, UserRequestAdmin userRequestAdmin) {; + User user = userServiceHelper.getUserEntityById(userId); + User updatedUser = userServiceHelper.updateUserDataAdmin(userRequestAdmin, user); return userMapper.toResponse(updatedUser); } @@ -76,7 +69,7 @@ public void deleteUserById(Long id){ } if (!userRepository.existsById(id)) { - throw new RuntimeException("User with id " + id + " does not exist"); + throw new UserNotFoundByIdException(id); } User replacementUser = userRepository.findById(1L) @@ -91,6 +84,7 @@ public void deleteUserById(Long id){ List reservationList = reservationRepository.findByUser_Id(id); if (!reservationList.isEmpty()) { reservationList.forEach(reservation -> reservation.setUser(replacementUser)); + reservationList.forEach(reservation -> reservation.setBookingStatus(BookingStatus.CANCELLED)); reservationRepository.saveAll(reservationList); } @@ -99,7 +93,7 @@ public void deleteUserById(Long id){ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - User user = userServiceHelper.findByUsername(username); + User user = userServiceHelper.getUserEntityByUsername(username); return new CustomUserDetails(user); } } diff --git a/src/main/java/com/SleepUp/SU/user/dto/UserRequest.java b/src/main/java/com/SleepUp/SU/user/dto/UserRequest.java index 2410cf2f..de4497ee 100644 --- a/src/main/java/com/SleepUp/SU/user/dto/UserRequest.java +++ b/src/main/java/com/SleepUp/SU/user/dto/UserRequest.java @@ -4,8 +4,9 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; +import lombok.Builder; - +@Builder public record UserRequest( @NotBlank(message = "Username cannot be blank") @Size(max = 50, message = "Username cannot be longer than 50 characters!") @Schema(description = "The username for user", example = "user1", required = true) diff --git a/src/main/java/com/SleepUp/SU/user/dto/UserRequestAdmin.java b/src/main/java/com/SleepUp/SU/user/dto/UserRequestAdmin.java index 2842daf2..ba45dac6 100644 --- a/src/main/java/com/SleepUp/SU/user/dto/UserRequestAdmin.java +++ b/src/main/java/com/SleepUp/SU/user/dto/UserRequestAdmin.java @@ -6,7 +6,9 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import lombok.Builder; +@Builder public record UserRequestAdmin( @NotBlank(message = "Username cannot be blank") @Size(max = 50, message = "Username cannot be longer than 50 characters!") String username, diff --git a/src/main/java/com/SleepUp/SU/user/user/UserUserController.java b/src/main/java/com/SleepUp/SU/user/user/UserUserController.java index 38ce596e..1659bea7 100644 --- a/src/main/java/com/SleepUp/SU/user/user/UserUserController.java +++ b/src/main/java/com/SleepUp/SU/user/user/UserUserController.java @@ -3,7 +3,10 @@ import com.SleepUp.SU.user.entity.CustomUserDetails; import com.SleepUp.SU.user.dto.UserRequest; import com.SleepUp.SU.user.dto.UserResponse; -import com.SleepUp.SU.utils.ApiMessageDto; +import com.SleepUp.SU.utils.dto.ApiMessageDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -11,8 +14,9 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +@Tag(name = "User", description = "Operations related to the authenticated user's account") @RestController -@RequestMapping("/api/users/me") +@RequestMapping("/users/me") @PreAuthorize("isAuthenticated()") @RequiredArgsConstructor public class UserUserController { @@ -20,12 +24,29 @@ public class UserUserController { private final UserUserService userUserService; @GetMapping + @Operation(summary = "Get Logged-in User Info", description = "Retrieve information about the currently authenticated user.", + responses = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved user information"), + @ApiResponse(responseCode = "401", ref = "#/components/responses/Unauthorized"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) @ResponseStatus(HttpStatus.OK) public UserResponse getLoggedUser(@AuthenticationPrincipal CustomUserDetails customUserDetails){ return userUserService.getLoggedUser(customUserDetails.getId()); } @PutMapping + @Operation(summary = "Update Logged-in User Info", description = "Update the information of the currently authenticated user.", + responses = { + @ApiResponse(responseCode = "200", description = "Successfully updated user information"), + @ApiResponse(responseCode = "400", ref = "#/components/responses/BadRequest"), + @ApiResponse(responseCode = "401", ref = "#/components/responses/Unauthorized"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "404", ref = "#/components/responses/UserFound"), + @ApiResponse(responseCode = "409", ref = "#/components/responses/Conflict"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) @ResponseStatus(HttpStatus.OK) public UserResponse putLoggedUser(@AuthenticationPrincipal CustomUserDetails customUserDetails, @RequestBody @Valid UserRequest userRequest){ @@ -33,6 +54,14 @@ public UserResponse putLoggedUser(@AuthenticationPrincipal CustomUserDetails cus } @DeleteMapping + @Operation(summary = "Delete Logged-in User Account", description = "Delete the account of the currently authenticated user.", + responses = { + @ApiResponse(responseCode = "200", description = "Successfully deleted user account"), + @ApiResponse(responseCode = "401", ref = "#/components/responses/Unauthorized"), + @ApiResponse(responseCode = "403", ref = "#/components/responses/Forbidden"), + @ApiResponse(responseCode = "404", ref = "#/components/responses/UserFound"), + @ApiResponse(responseCode = "500", ref = "#/components/responses/InternalServerError") + }) @ResponseStatus(HttpStatus.OK) public ApiMessageDto deleteLoggedUser(@AuthenticationPrincipal CustomUserDetails customUserDetails){ userUserService.deleteMyUser(customUserDetails.getId()); diff --git a/src/main/java/com/SleepUp/SU/user/user/UserUserServiceImpl.java b/src/main/java/com/SleepUp/SU/user/user/UserUserServiceImpl.java index eb303ae6..01f19e2a 100644 --- a/src/main/java/com/SleepUp/SU/user/user/UserUserServiceImpl.java +++ b/src/main/java/com/SleepUp/SU/user/user/UserUserServiceImpl.java @@ -4,6 +4,7 @@ import com.SleepUp.SU.accommodation.repository.AccommodationRepository; import com.SleepUp.SU.reservation.entity.Reservation; import com.SleepUp.SU.reservation.repository.ReservationRepository; +import com.SleepUp.SU.reservation.status.BookingStatus; import com.SleepUp.SU.user.entity.User; import com.SleepUp.SU.user.repository.UserRepository; import com.SleepUp.SU.user.dto.UserMapper; @@ -30,12 +31,12 @@ public class UserUserServiceImpl implements UserUserService{ @Override public UserResponse getLoggedUser(Long id){ - return userMapper.toResponse(userServiceHelper.findById(id)); + return userMapper.toResponse(userServiceHelper.getUserEntityById(id)); } @Override public UserResponse updateLoggedUser(UserRequest userRequest, Long id){ - User user = userServiceHelper.findById(id); + User user = userServiceHelper.getUserEntityById(id); userServiceHelper.updateUserData(userRequest, user); return (userMapper.toResponse(user)); } @@ -56,9 +57,10 @@ public void deleteMyUser(Long id){ List reservationList = reservationRepository.findByUser_Id(id); if (!reservationList.isEmpty()) { reservationList.forEach(reservation -> reservation.setUser(replacementUser)); + reservationList.forEach(reservation -> reservation.setBookingStatus(BookingStatus.CANCELLED)); reservationRepository.saveAll(reservationList); } - userRepository.deleteById(userServiceHelper.findById(id).getId()); + userRepository.deleteById(userServiceHelper.getUserEntityById(id).getId()); } } diff --git a/src/main/java/com/SleepUp/SU/user/utils/UserServiceHelper.java b/src/main/java/com/SleepUp/SU/user/utils/UserServiceHelper.java index 078c75f0..434b888f 100644 --- a/src/main/java/com/SleepUp/SU/user/utils/UserServiceHelper.java +++ b/src/main/java/com/SleepUp/SU/user/utils/UserServiceHelper.java @@ -1,10 +1,12 @@ package com.SleepUp.SU.user.utils; +import com.SleepUp.SU.user.dto.UserMapper; import com.SleepUp.SU.user.entity.User; import com.SleepUp.SU.user.repository.UserRepository; import com.SleepUp.SU.user.dto.UserRequest; import com.SleepUp.SU.user.dto.UserRequestAdmin; import com.SleepUp.SU.user.role.Role; +import com.SleepUp.SU.utils.EntityUtil; import com.SleepUp.SU.utils.exceptions.UserEmailAlreadyExistsException; import com.SleepUp.SU.utils.exceptions.UserNotFoundByIdException; import com.SleepUp.SU.utils.exceptions.UserNotFoundByUsernameException; @@ -19,22 +21,31 @@ public class UserServiceHelper { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; + private final UserMapper userMapper; + private final EntityUtil entityUtil; - - public User findById(Long id) { + public User getUserEntityById(Long id) { return userRepository.findById(id) .orElseThrow(() -> new UserNotFoundByIdException(id)); } - public User findByUsername(String username) { + public User getUserEntityByUsername(String username) { return userRepository.findByUsername(username) .orElseThrow(() -> new UserNotFoundByUsernameException(username)); } public void validateUserDoesNotExist(String username, String email) { + validateUsernameDoesNotExist(username); + validateEmailDoesNotExist(email); + } + + public void validateUsernameDoesNotExist(String username) { if (userRepository.existsByUsername(username)) { throw new UserUsernameAlreadyExistsException(username); } + } + + public void validateEmailDoesNotExist(String email) { if (userRepository.existsByEmail(email)) { throw new UserEmailAlreadyExistsException(email); } @@ -46,41 +57,44 @@ public String getEncodePassword(String password) { @Transactional - public void updateUserData(UserRequest request, User user) { - User isExistingUsername = userRepository.findByUsername(request.username()) - .orElse(user); - User isExistingEmail = userRepository.findByEmail(request.email()) - .orElse(user); - - if (isExistingUsername.getId() != user.getId() || isExistingEmail.getId() != user.getId()){ - throw new RuntimeException("This username or email already exist"); - } + public User createUser(UserRequest request, Role role){ + validateUserDoesNotExist(request.username(), request.email()); + String encodedPassword = passwordEncoder.encode(request.password()); + User user = userMapper.toEntity(request, encodedPassword, role); + return userRepository.save(user); + } + public User updateUser(UserRequest request, User existingUser) { - String username = request.username() != null && !request.username().isEmpty() - ? request.username() : - user.getUsername(); + if(!request.username().equals(existingUser.getUsername())){ + validateUsernameDoesNotExist(request.username()); + } - String name = request.name() != null && !request.name().isEmpty() - ? request.name() : - user.getName(); + if(!request.email().equals(existingUser.getEmail())){ + validateUsernameDoesNotExist(request.username()); + } - String email = request.email() != null && !request.email().isEmpty() - ? request.email() : - user.getEmail(); + entityUtil.updateField(request.username(), existingUser::getUsername, existingUser::setUsername); + entityUtil.updateField(request.email(), existingUser::getEmail, existingUser::setEmail); + entityUtil.updateField(request.name(), existingUser::getName, existingUser::setName); String password = request.password() != null && !request.password().isEmpty() ? this.getEncodePassword(request.password()) : - user.getPassword(); + existingUser.getPassword(); + + existingUser.setPassword(password); + + return existingUser; - user.setUsername(username); - user.setName(name); - user.setEmail(email); - user.setPassword(password); } @Transactional - public void updateUserDataAdmin(UserRequestAdmin userRequestAdmin, User user){ + public User updateUserData(UserRequest request, User user) { + return updateUser(request, user); + } + + @Transactional + public User updateUserDataAdmin(UserRequestAdmin userRequestAdmin, User user){ UserRequest userData = new UserRequest( userRequestAdmin.username(), userRequestAdmin.name(), @@ -88,14 +102,14 @@ public void updateUserDataAdmin(UserRequestAdmin userRequestAdmin, User user){ userRequestAdmin.password() ); - updateUserData(userData, user); + User updatedUser = updateUser(userData, user); Role role = userRequestAdmin.role() != null && !userRequestAdmin.role().getRoleName().isEmpty() ? userRequestAdmin.role() : - user.getRole(); + updatedUser.getRole(); - user.setRole(role); + updatedUser.setRole(role); + return updatedUser; } - } diff --git a/src/main/java/com/SleepUp/SU/utils/Schedule/ReservationReminderTask.java b/src/main/java/com/SleepUp/SU/utils/Schedule/ReservationReminderTask.java index 0fc81cfc..b3c245ef 100644 --- a/src/main/java/com/SleepUp/SU/utils/Schedule/ReservationReminderTask.java +++ b/src/main/java/com/SleepUp/SU/utils/Schedule/ReservationReminderTask.java @@ -47,18 +47,13 @@ public void sendReservationReminders() { for (Reservation reservation : upcomingReservations) { try { if (emailServiceHelper.canSendReservationEmails( - reservation.getAccommodation().getManagedBy(), - reservation.getAccommodation(), reservation)) { emailServiceHelper.sendReservationReminders( - reservation.getAccommodation().getManagedBy(), - reservation.getAccommodation(), reservation ); emailServiceHelper.sendOwnerReservationReminderEmail( - reservation.getAccommodation(), reservation); successCount++; diff --git a/src/main/java/com/SleepUp/SU/utils/ApiMessageDto.java b/src/main/java/com/SleepUp/SU/utils/dto/ApiMessageDto.java similarity index 59% rename from src/main/java/com/SleepUp/SU/utils/ApiMessageDto.java rename to src/main/java/com/SleepUp/SU/utils/dto/ApiMessageDto.java index af169105..14fe9a33 100644 --- a/src/main/java/com/SleepUp/SU/utils/ApiMessageDto.java +++ b/src/main/java/com/SleepUp/SU/utils/dto/ApiMessageDto.java @@ -1,4 +1,4 @@ -package com.SleepUp.SU.utils; +package com.SleepUp.SU.utils.dto; public record ApiMessageDto(String message) { } diff --git a/src/main/java/com/SleepUp/SU/utils/email/EmailService.java b/src/main/java/com/SleepUp/SU/utils/email/EmailService.java index 390c7f6d..5abc2730 100644 --- a/src/main/java/com/SleepUp/SU/utils/email/EmailService.java +++ b/src/main/java/com/SleepUp/SU/utils/email/EmailService.java @@ -1,45 +1,27 @@ package com.SleepUp.SU.utils.email; -import com.SleepUp.SU.accommodation.entity.Accommodation; import com.SleepUp.SU.reservation.entity.Reservation; import com.SleepUp.SU.user.entity.User; import jakarta.mail.MessagingException; -import jakarta.mail.internet.MimeMessage; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; -import org.thymeleaf.context.Context; -import org.thymeleaf.spring6.SpringTemplateEngine; + +import java.math.BigDecimal; @Service public interface EmailService { - void sendWelcomeEmail(String toEmail, String userName) throws MessagingException; - - void sendOwnerReservedNotification(User guest, Accommodation accommodation, Reservation reservation) throws MessagingException; - - void sendReservationConfirmationEmail(String toEmail, String userName, String accommodationName, String location, String checkInDate, String checkOutDate) throws MessagingException; - - void sendReservationReminderEmail(String toEmail, String guestName, String accommodationName, String location, String checkInDate, String checkOutDate) throws MessagingException; - - void sendOwnerReservationReminderEmail(String toEmail, String ownerName, String accommodationName,String location, String checkInDate, String checkOutDate) throws MessagingException; - - void sendCancellationConfirmationEmail(String toEmail, String userName, String accommodationName, String location, String checkInDate, String checkOutDate) throws MessagingException; - - void sendCancellationByOwnerNotificationEmail(String toEmail, String userName, String accommodationName, String location, String checkInDate, String checkOutDate) throws MessagingException; + void sendWelcomeEmail(User user) throws MessagingException; - void sendCancellationNotificationToOwnerEmail(String toEmail, String userName, String accommodationName, String location, String checkInDate, String checkOutDate) throws MessagingException; + void sendOwnerReservedNotification(Reservation reservation) throws MessagingException; - void sendReservationConfirmationEmail(User guest, Accommodation accommodation, Reservation reservation) throws MessagingException; + void sendGuestReservationConfirmationEmail(Reservation reservation, BigDecimal discountAmount) throws MessagingException; - void sendReservationReminderEmail(User guest, Accommodation accommodation, Reservation reservation) throws MessagingException; + void sendReservationReminderEmail(Reservation reservation) throws MessagingException; - void sendOwnerReservationReminderEmail(Accommodation accommodation, Reservation reservation) throws MessagingException; + void sendOwnerReservationReminderEmail(Reservation reservation) throws MessagingException; - void sendCancellationConfirmationEmail(User guest, Accommodation accommodation, Reservation reservation) throws MessagingException; + void sendCancellationConfirmationEmail(Reservation reservation) throws MessagingException; - void sendCancellationByOwnerNotificationEmail(User guest, Accommodation accommodation, Reservation reservation) throws MessagingException; + void sendCancellationByOwnerNotificationEmail(Reservation reservation) throws MessagingException; - void sendCancellationNotificationToOwnerEmail(User guest, Accommodation accommodation, Reservation reservation) throws MessagingException; -} \ No newline at end of file + void sendCancellationNotificationToOwnerEmail(Reservation reservation) throws MessagingException; +} diff --git a/src/main/java/com/SleepUp/SU/utils/email/EmailServiceHelper.java b/src/main/java/com/SleepUp/SU/utils/email/EmailServiceHelper.java index 2747d281..47d207b5 100644 --- a/src/main/java/com/SleepUp/SU/utils/email/EmailServiceHelper.java +++ b/src/main/java/com/SleepUp/SU/utils/email/EmailServiceHelper.java @@ -2,30 +2,29 @@ import com.SleepUp.SU.accommodation.entity.Accommodation; import com.SleepUp.SU.auth.AuthService; -import com.SleepUp.SU.config.AppProperties; +import com.SleepUp.SU.config.properties.MailProperties; import com.SleepUp.SU.reservation.entity.Reservation; import com.SleepUp.SU.user.entity.User; -import com.SleepUp.SU.user.dto.UserRequest; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import java.math.BigDecimal; + @Service @RequiredArgsConstructor public class EmailServiceHelper { private final EmailService emailService; - private final Logger logger = LoggerFactory.getLogger(AuthService.class); + public Logger logger = LoggerFactory.getLogger(AuthService.class); - private final AppProperties appProperties; - private AppProperties.MailProperties mailProperties; + public MailProperties mailProperties; private boolean emailEnabled; @PostConstruct public void validateEmailConfig() { - mailProperties = appProperties.getMail(); // Access nested MailProperties emailEnabled = mailProperties != null && mailProperties.getFrom() != null && !mailProperties.getFrom().isBlank() && mailProperties.getUsername() != null && !mailProperties.getUsername().isBlank() @@ -38,118 +37,113 @@ public void validateEmailConfig() { } } - public void sendWelcomeEmail(UserRequest userRequest, User user) { + public void sendWelcomeEmail(User user) { try { - emailService.sendWelcomeEmail(userRequest.email(), userRequest.username()); + emailService.sendWelcomeEmail(user); logger.info("Welcome email sent successfully to: {}", user.getEmail()); } catch (Exception e) { logger.warn("Failed to send welcome email to {}: {}", user.getEmail(), e.getMessage()); } } - public void sendOwnerReservedNotification(User user, Accommodation accommodation, Reservation reservation) { + public void sendOwnerReservedNotification(Reservation reservation) { try { - emailService.sendOwnerReservedNotification(user, accommodation, reservation); - logger.info("New reservation sent successfully to: {}", user.getEmail()); + emailService.sendOwnerReservedNotification(reservation); + logger.info("New reservation notification email sent successfully to: {}", reservation.getAccommodation().getManagedBy().getEmail()); } catch (Exception e) { - logger.warn("Failed to send new reservation email to {}: {}", user.getEmail(), e.getMessage()); + logger.warn("Failed to send new reservation email to {}: {}", reservation.getAccommodation().getManagedBy().getEmail(), e.getMessage()); } } - public void sendReservationConfirmationEmail(User guest, Accommodation accommodation, Reservation reservation) { + public void sendReservationConfirmationEmail(Reservation reservation, BigDecimal discountAmount) { try { - emailService.sendReservationConfirmationEmail(guest, accommodation, reservation); - logger.info("Reservation confirmation email sent successfully to: {}", guest.getEmail()); + emailService.sendGuestReservationConfirmationEmail(reservation, discountAmount); + logger.info("Reservation confirmation email sent successfully to: {}", reservation.getUser().getEmail()); } catch (Exception e) { - logger.warn("Failed to send reservation confirmation email to {}: {}", guest.getEmail(), e.getMessage()); + logger.warn("Failed to send reservation confirmation email to {}: {}", reservation.getUser().getEmail(), e.getMessage()); } } - public void sendReservationReminderEmail(User guest, Accommodation accommodation, Reservation reservation) { + public void sendReservationReminderEmail(Reservation reservation) { try { - emailService.sendReservationReminderEmail(guest, accommodation, reservation); - logger.info("Reservation reminder email sent successfully to: {}", guest.getEmail()); + emailService.sendReservationReminderEmail(reservation); + logger.info("Reservation reminder email sent successfully to: {}", reservation.getUser().getEmail()); } catch (Exception e) { - logger.warn("Failed to send reservation reminder email to {}: {}", guest.getEmail(), e.getMessage()); + logger.warn("Failed to send reservation reminder email to {}: {}", reservation.getUser().getEmail(), e.getMessage()); } } - public void sendOwnerReservationReminderEmail(Accommodation accommodation, Reservation reservation) { + public void sendOwnerReservationReminderEmail(Reservation reservation) { try { - emailService.sendOwnerReservationReminderEmail(accommodation, reservation); - logger.info("Owner reservation reminder email sent successfully to: {}", - accommodation.getManagedBy().getEmail()); + emailService.sendOwnerReservationReminderEmail(reservation); + logger.info("Owner reservation reminder email sent successfully to: {}", reservation.getAccommodation().getManagedBy().getEmail()); } catch (Exception e) { - logger.warn("Failed to send owner reservation reminder email to {}: {}", - accommodation.getManagedBy().getEmail(), e.getMessage()); + logger.warn("Failed to send owner reservation reminder email to {}: {}", reservation.getAccommodation().getManagedBy().getEmail(), e.getMessage()); } } - public void sendCancellationConfirmationEmail(User guest, Accommodation accommodation, Reservation reservation) { + public void sendCancellationConfirmationEmail(Reservation reservation) { try { - emailService.sendCancellationConfirmationEmail(guest, accommodation, reservation); - logger.info("Cancellation confirmation email sent successfully to: {}", guest.getEmail()); + emailService.sendCancellationConfirmationEmail(reservation); + logger.info("Cancellation confirmation email sent successfully to: {}", reservation.getUser().getEmail()); } catch (Exception e) { - logger.warn("Failed to send cancellation confirmation email to {}: {}", guest.getEmail(), e.getMessage()); + logger.warn("Failed to send cancellation confirmation email to {}: {}", reservation.getUser().getEmail(), e.getMessage()); } } - public void sendCancellationByOwnerNotificationEmail(User guest, Accommodation accommodation, Reservation reservation) { + public void sendCancellationByOwnerNotificationEmail(Reservation reservation) { try { - emailService.sendCancellationByOwnerNotificationEmail(guest, accommodation, reservation); - logger.info("Cancellation by owner notification email sent successfully to: {}", guest.getEmail()); + emailService.sendCancellationByOwnerNotificationEmail(reservation); + logger.info("Cancellation by owner notification email sent successfully to: {}", reservation.getUser().getEmail()); } catch (Exception e) { - logger.warn("Failed to send cancellation by owner notification email to {}: {}", - guest.getEmail(), e.getMessage()); + logger.warn("Failed to send cancellation by owner notification email to {}: {}", reservation.getUser().getEmail(), e.getMessage()); } } - public void sendCancellationNotificationToOwnerEmail(User guest, Accommodation accommodation, Reservation reservation) { + public void sendCancellationNotificationToOwnerEmail(Reservation reservation) { try { - emailService.sendCancellationNotificationToOwnerEmail(guest, accommodation, reservation); - logger.info("Cancellation notification to owner email sent successfully to: {}", - accommodation.getManagedBy().getEmail()); + emailService.sendCancellationNotificationToOwnerEmail(reservation); + logger.info("Cancellation notification to owner email sent successfully to: {}", reservation.getAccommodation().getManagedBy().getEmail()); } catch (Exception e) { - logger.warn("Failed to send cancellation notification to owner email to {}: {}", - accommodation.getManagedBy().getEmail(), e.getMessage()); + logger.warn("Failed to send cancellation notification to owner email to {}: {}", reservation.getAccommodation().getManagedBy().getEmail(), e.getMessage()); } } - public void handleNewReservationEmails(User guest, Accommodation accommodation, Reservation reservation) { + public void handleNewReservationEmails(Reservation reservation, BigDecimal discountAmount) { logger.info("Processing emails for new reservation - Guest: {}, Accommodation: {}", - guest.getEmail(), accommodation.getName()); + reservation.getUser().getEmail(), reservation.getAccommodation().getName()); - sendReservationConfirmationEmail(guest, accommodation, reservation); - sendOwnerReservedNotification(guest, accommodation, reservation); + sendReservationConfirmationEmail(reservation, discountAmount); + sendOwnerReservedNotification(reservation); logger.info("New reservation email processing completed"); } - public void handleGuestCancellationEmails(User guest, Accommodation accommodation, Reservation reservation) { + public void handleGuestCancellationEmails(Reservation reservation) { logger.info("Processing emails for guest cancellation - Guest: {}, Accommodation: {}", - guest.getEmail(), accommodation.getName()); + reservation.getUser().getEmail(), reservation.getAccommodation().getName()); - sendCancellationConfirmationEmail(guest, accommodation, reservation); - sendCancellationNotificationToOwnerEmail(guest, accommodation, reservation); + sendCancellationConfirmationEmail(reservation); + sendCancellationNotificationToOwnerEmail(reservation); logger.info("Guest cancellation email processing completed"); } - public void handleOwnerCancellationEmails(User guest, Accommodation accommodation, Reservation reservation) { + public void handleOwnerCancellationEmails(Reservation reservation) { logger.info("Processing emails for owner cancellation - Guest: {}, Accommodation: {}", - guest.getEmail(), accommodation.getName()); + reservation.getUser().getEmail(), reservation.getAccommodation().getName()); - sendCancellationByOwnerNotificationEmail(guest, accommodation, reservation); + sendCancellationByOwnerNotificationEmail(reservation); logger.info("Owner cancellation email processing completed"); } - public void sendReservationReminders(User guest, Accommodation accommodation, Reservation reservation) { + public void sendReservationReminders(Reservation reservation) { logger.info("Sending reservation reminders - Guest: {}, Owner: {}, Accommodation: {}", - guest.getEmail(), accommodation.getManagedBy().getEmail(), accommodation.getName()); + reservation.getUser().getEmail(), reservation.getAccommodation().getManagedBy().getEmail(), reservation.getAccommodation().getName()); - sendReservationReminderEmail(guest, accommodation, reservation); - sendOwnerReservationReminderEmail(accommodation, reservation); + sendReservationReminderEmail(reservation); + sendOwnerReservationReminderEmail(reservation); logger.info("Reservation reminders sent successfully"); } @@ -162,11 +156,17 @@ public boolean canSendEmails(User user) { return true; } - public boolean canSendReservationEmails(User guest, Accommodation accommodation, Reservation reservation) { - if (!canSendEmails(guest)) { + public boolean canSendReservationEmails(Reservation reservation) { + if (reservation == null) { + logger.warn("Cannot send reservation emails: Reservation is null"); + return false; + } + + if (!canSendEmails(reservation.getUser())) { return false; } + Accommodation accommodation = reservation.getAccommodation(); if (accommodation == null || accommodation.getManagedBy() == null) { logger.warn("Cannot send reservation emails: Accommodation or owner is null"); return false; @@ -177,11 +177,6 @@ public boolean canSendReservationEmails(User guest, Accommodation accommodation, return false; } - if (reservation == null) { - logger.warn("Cannot send reservation emails: Reservation is null"); - return false; - } - return true; } } diff --git a/src/main/java/com/SleepUp/SU/utils/email/EmailServiceImpl.java b/src/main/java/com/SleepUp/SU/utils/email/EmailServiceImpl.java index 3d46ffb8..4da1e66c 100644 --- a/src/main/java/com/SleepUp/SU/utils/email/EmailServiceImpl.java +++ b/src/main/java/com/SleepUp/SU/utils/email/EmailServiceImpl.java @@ -13,10 +13,12 @@ import org.thymeleaf.context.Context; import org.thymeleaf.spring6.SpringTemplateEngine; +import java.math.BigDecimal; + @Service @RequiredArgsConstructor @Slf4j -public class EmailServiceImpl implements EmailService{ +public class EmailServiceImpl implements EmailService { private static final String UTF8_ENCODING = "UTF-8"; private static final String DASHBOARD_URL = "http://localhost:8080/swagger-ui/index.html#/"; @@ -25,266 +27,128 @@ public class EmailServiceImpl implements EmailService{ private final SpringTemplateEngine templateEngine; @Override - public void sendWelcomeEmail(String toEmail, String userName) throws MessagingException { - MimeMessage message = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message, true, UTF8_ENCODING); - - Context context = new Context(); - context.setVariable("userName", userName); - context.setVariable("dashboardUrl", DASHBOARD_URL); - - String htmlContent = templateEngine.process("WelcomeUser", context); + public void sendWelcomeEmail(User user) throws MessagingException { + Context context = createContext(); + context.setVariable("userName", user.getName()); - helper.setTo(toEmail); - helper.setSubject("Welcome to SleepUp!"); - helper.setText(htmlContent, true); - - mailSender.send(message); - log.info("Welcome email sent successfully to: {}", toEmail); + sendEmail(user.getEmail(), "Welcome to SleepUp!", "WelcomeUser", context); } @Override - public void sendOwnerReservedNotification(User guest, Accommodation accommodation, Reservation reservation) throws MessagingException { - MimeMessage message = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message, true, UTF8_ENCODING); - - Context context = new Context(); - context.setVariable("ownerName", accommodation.getManagedBy().getName()); - context.setVariable("accommodationName", accommodation.getName()); - context.setVariable("location", accommodation.getLocation()); + public void sendOwnerReservedNotification(Reservation reservation) throws MessagingException { + User owner = getOwner(reservation); + User guest = getGuest(reservation); + Context context = createReservationContext(reservation); + context.setVariable("ownerName", owner.getName()); context.setVariable("guestName", guest.getName()); - context.setVariable("checkInDate", reservation.getCheckInDate()); - context.setVariable("checkOutDate", reservation.getCheckOutDate()); - context.setVariable("dashboardUrl", DASHBOARD_URL); - - String htmlContent = templateEngine.process("NotificationReservationOwner", context); - helper.setTo(accommodation.getManagedBy().getEmail()); - helper.setSubject("Your property has just been booked!"); - helper.setText(htmlContent, true); - - mailSender.send(message); - log.info("Owner notification email sent successfully to: {}", accommodation.getManagedBy().getEmail()); + sendEmail(owner.getEmail(), "Your property has just been booked!", "NotificationReservationOwner", context); } @Override - public void sendReservationConfirmationEmail(String toEmail, String userName, String accommodationName, - String location, String checkInDate, String checkOutDate) throws MessagingException { - MimeMessage message = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message, true, UTF8_ENCODING); + public void sendGuestReservationConfirmationEmail(Reservation reservation, BigDecimal discountAmount) throws MessagingException { + User guest = getGuest(reservation); + Context context = createReservationContext(reservation); + context.setVariable("userName", guest.getName()); + context.setVariable("discountAmount", discountAmount); - Context context = new Context(); - context.setVariable("userName", userName); - context.setVariable("accommodationName", accommodationName); - context.setVariable("location", location); - context.setVariable("checkInDate", checkInDate); - context.setVariable("checkOutDate", checkOutDate); - context.setVariable("dashboardUrl", DASHBOARD_URL); - - String htmlContent = templateEngine.process("ConfirmationGuest", context); - - helper.setTo(toEmail); - helper.setSubject("Your Booking Confirmation!"); - helper.setText(htmlContent, true); - - mailSender.send(message); - log.info("Reservation confirmation email sent successfully to: {}", toEmail); + sendEmail(guest.getEmail(), "Your Booking Confirmation!", "ConfirmationGuest", context); } - @Override - public void sendReservationReminderEmail(String toEmail, String guestName, String accommodationName, - String location, String checkInDate, String checkOutDate) throws MessagingException { - MimeMessage message = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message, true, UTF8_ENCODING); - - Context context = new Context(); - context.setVariable("guestName", guestName); - context.setVariable("accommodationName", accommodationName); - context.setVariable("location", location); - context.setVariable("checkInDate", checkInDate); - context.setVariable("checkOutDate", checkOutDate); - context.setVariable("dashboardUrl", DASHBOARD_URL); - - String htmlContent = templateEngine.process("ReminderGuestReservation", context); - - helper.setTo(toEmail); - helper.setSubject("Upcoming Reservation Reminder"); - helper.setText(htmlContent, true); + public void sendReservationReminderEmail(Reservation reservation) throws MessagingException { + User guest = getGuest(reservation); + Context context = createReservationContext(reservation); + context.setVariable("guestName", guest.getName()); - mailSender.send(message); - log.info("Reservation reminder email sent successfully to: {}", toEmail); + sendEmail(guest.getEmail(), "Upcoming Reservation Reminder", "ReminderGuestReservation", context); } @Override - public void sendOwnerReservationReminderEmail(String toEmail, String ownerName, String accommodationName, - String location, String checkInDate, String checkOutDate) throws MessagingException { - MimeMessage message = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message, true, UTF8_ENCODING); - - Context context = new Context(); - context.setVariable("ownerName", ownerName); - context.setVariable("accommodationName", accommodationName); - context.setVariable("location", location); - context.setVariable("checkInDate", checkInDate); - context.setVariable("checkOutDate", checkOutDate); - context.setVariable("dashboardUrl", DASHBOARD_URL); - - String htmlContent = templateEngine.process("ReminderOwnerReservation", context); - - helper.setTo(toEmail); - helper.setSubject("Upcoming Reservation at Your Property"); - helper.setText(htmlContent, true); + public void sendOwnerReservationReminderEmail(Reservation reservation) throws MessagingException { + User owner = getOwner(reservation); + Context context = createReservationContext(reservation); + context.setVariable("ownerName", owner.getName()); - mailSender.send(message); - log.info("Owner reservation reminder email sent successfully to: {}", toEmail); + sendEmail(owner.getEmail(), "Upcoming Reservation at Your Property", "ReminderOwnerReservation", context); } @Override - public void sendCancellationConfirmationEmail(String toEmail, String userName, String accommodationName, - String location, String checkInDate, String checkOutDate) throws MessagingException { - MimeMessage message = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message, true, UTF8_ENCODING); - - Context context = new Context(); - context.setVariable("userName", userName); - context.setVariable("accommodationName", accommodationName); - context.setVariable("location", location); - context.setVariable("checkInDate", checkInDate); - context.setVariable("checkOutDate", checkOutDate); + public void sendCancellationConfirmationEmail(Reservation reservation) throws MessagingException { + User guest = getGuest(reservation); + Accommodation accommodation = reservation.getAccommodation(); + Context context = createCancellationContext(guest, accommodation, reservation); - String htmlContent = templateEngine.process("CancellationGuest", context); - - helper.setTo(toEmail); - helper.setSubject("Your reservation has been successfully cancelled"); - helper.setText(htmlContent, true); - - mailSender.send(message); - log.info("Cancellation confirmation email sent successfully to: {}", toEmail); + sendEmail(guest.getEmail(), "Your reservation has been successfully cancelled", "CancellationGuest", context); } @Override - public void sendCancellationByOwnerNotificationEmail(String toEmail, String userName, String accommodationName, - String location, String checkInDate, String checkOutDate) throws MessagingException { - MimeMessage message = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message, true, UTF8_ENCODING); + public void sendCancellationByOwnerNotificationEmail(Reservation reservation) throws MessagingException { + User guest = getGuest(reservation); + Accommodation accommodation = reservation.getAccommodation(); + Context context = createCancellationContext(guest, accommodation, reservation); - Context context = new Context(); - context.setVariable("userName", userName); - context.setVariable("accommodationName", accommodationName); - context.setVariable("location", location); - context.setVariable("checkInDate", checkInDate); - context.setVariable("checkOutDate", checkOutDate); - - String htmlContent = templateEngine.process("CancellationGuestByOwner", context); - - helper.setTo(toEmail); - helper.setSubject("Your reservation has been cancelled"); - helper.setText(htmlContent, true); - - mailSender.send(message); - log.info("Cancellation by owner notification email sent successfully to: {}", toEmail); + sendEmail(guest.getEmail(), "Your reservation has been cancelled", "CancellationGuestByOwner", context); } @Override - public void sendCancellationNotificationToOwnerEmail(String toEmail, String userName, String accommodationName, - String location, String checkInDate, String checkOutDate) throws MessagingException { - MimeMessage message = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message, true, UTF8_ENCODING); - - Context context = new Context(); - context.setVariable("userName", userName); - context.setVariable("accommodationName", accommodationName); - context.setVariable("location", location); - context.setVariable("checkInDate", checkInDate); - context.setVariable("checkOutDate", checkOutDate); - - String htmlContent = templateEngine.process("CancellationByGuestOwner", context); + public void sendCancellationNotificationToOwnerEmail(Reservation reservation) throws MessagingException { + User guest = getGuest(reservation); + User owner = getOwner(reservation); + Accommodation accommodation = reservation.getAccommodation(); + Context context = createCancellationContext(guest, accommodation, reservation); - helper.setTo(toEmail); - helper.setSubject("Reservation at your property has been cancelled"); - helper.setText(htmlContent, true); + sendEmail(owner.getEmail(), "Reservation at your property has been cancelled", "CancellationByGuestOwner", context); + } - mailSender.send(message); - log.info("Cancellation notification to owner email sent successfully to: {}", toEmail); + private Context createContext() { + Context context = new Context(); + context.setVariable("dashboardUrl", DASHBOARD_URL); + return context; } + private Context createReservationContext(Reservation reservation) { + Context context = createContext(); + Accommodation accommodation = reservation.getAccommodation(); - @Override - public void sendReservationConfirmationEmail(User guest, Accommodation accommodation, Reservation reservation) throws MessagingException { - sendReservationConfirmationEmail( - guest.getEmail(), - guest.getName(), - accommodation.getName(), - accommodation.getLocation(), - reservation.getCheckInDate().toString(), - reservation.getCheckOutDate().toString() - ); - } + context.setVariable("accommodationName", accommodation.getName()); + context.setVariable("location", accommodation.getLocation()); + context.setVariable("checkInDate", reservation.getCheckInDate()); + context.setVariable("checkOutDate", reservation.getCheckOutDate()); + context.setVariable("amount", reservation.getTotalPrice()); - @Override - public void sendReservationReminderEmail(User guest, Accommodation accommodation, Reservation reservation) throws MessagingException { - sendReservationReminderEmail( - guest.getEmail(), - guest.getName(), - accommodation.getName(), - accommodation.getLocation(), - reservation.getCheckInDate().toString(), - reservation.getCheckOutDate().toString() - ); + return context; } - - @Override - public void sendOwnerReservationReminderEmail(Accommodation accommodation, Reservation reservation) throws MessagingException { - User owner = accommodation.getManagedBy(); - sendOwnerReservationReminderEmail( - owner.getEmail(), - owner.getName(), - accommodation.getName(), - accommodation.getLocation(), - reservation.getCheckInDate().toString(), - reservation.getCheckOutDate().toString() - ); + private Context createCancellationContext(User guest, Accommodation accommodation, Reservation reservation) { + Context context = createContext(); + context.setVariable("userName", guest.getName()); + context.setVariable("accommodationName", accommodation.getName()); + context.setVariable("location", accommodation.getLocation()); + context.setVariable("checkInDate", reservation.getCheckInDate().toString()); + context.setVariable("checkOutDate", reservation.getCheckOutDate().toString()); + return context; } + private User getOwner(Reservation reservation) { + return reservation.getAccommodation().getManagedBy(); + } - @Override - public void sendCancellationConfirmationEmail(User guest, Accommodation accommodation, Reservation reservation) throws MessagingException { - sendCancellationConfirmationEmail( - guest.getEmail(), - guest.getName(), - accommodation.getName(), - accommodation.getLocation(), - reservation.getCheckInDate().toString(), - reservation.getCheckOutDate().toString() - ); + private User getGuest(Reservation reservation) { + return reservation.getUser(); } + private void sendEmail(String toEmail, String subject, String templateName, Context context) throws MessagingException { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, UTF8_ENCODING); - @Override - public void sendCancellationByOwnerNotificationEmail(User guest, Accommodation accommodation, Reservation reservation) throws MessagingException { - sendCancellationByOwnerNotificationEmail( - guest.getEmail(), - guest.getName(), - accommodation.getName(), - accommodation.getLocation(), - reservation.getCheckInDate().toString(), - reservation.getCheckOutDate().toString() - ); - } + String htmlContent = templateEngine.process(templateName, context); + helper.setTo(toEmail); + helper.setSubject(subject); + helper.setText(htmlContent, true); - @Override - public void sendCancellationNotificationToOwnerEmail(User guest, Accommodation accommodation, Reservation reservation) throws MessagingException { - User owner = accommodation.getManagedBy(); - sendCancellationNotificationToOwnerEmail( - owner.getEmail(), - guest.getName(), - accommodation.getName(), - accommodation.getLocation(), - reservation.getCheckInDate().toString(), - reservation.getCheckOutDate().toString() - ); + mailSender.send(message); + log.info("{} email sent successfully to: {}", subject, toEmail); } } diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index e65bb3c8..6a82e684 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -26,7 +26,6 @@ server.port=${SERVER_PORT} # Logging Configuration logging.level.org.springframework=INFO -logging.level.com.electronify=DEBUG # SWAGGER springdoc.swagger-ui.enabled=true @@ -53,4 +52,7 @@ jwt.refresh-expiration-ms=604800000 # Cloudinary configuration for testing cloudinary.cloud_name=${CLOUDINARY_CLOUD_NAME} cloudinary.api_key=${CLOUDINARY_API_KEY} -cloudinary.api_secret=${CLOUDINARY_API_SECRET} \ No newline at end of file +cloudinary.api_secret=${CLOUDINARY_API_SECRET} + +logging.level.org.springframework.security=DEBUG +logging.level.org.springframework.security.web=DEBUG \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9cb644a1..78c8d2ab 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,7 @@ spring.application.name=SU +spring.output.ansi.enabled=ALWAYS + # ENVIRONMENT VARIABLES spring.config.import=optional:file:.env[.properties] @@ -10,7 +12,7 @@ spring.datasource.password=${DB_PASSWORD} spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # JPA Configuration -spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect @@ -22,11 +24,10 @@ spring.sql.init.data-locations=classpath:data.sql spring.sql.init.continue-on-error=false # Server Configuration -server.port=${SERVER_PORT} +server.port=${SERVER_PORT:8080} # Logging Configuration logging.level.org.springframework=INFO -logging.level.com.electronify=DEBUG # SWAGGER springdoc.swagger-ui.enabled=true @@ -34,7 +35,7 @@ springdoc.api-docs.enabled=true # Email (SMTP) configuration spring.mail.host=smtp.gmail.com -spring.mail.port=587 +spring.mail.port=${SPRING_MAIL_PORT:587} spring.mail.from=${EMAIL} spring.mail.username=${EMAIL} spring.mail.password=${EMAIL_PASSWORD} @@ -47,8 +48,8 @@ spring.mail.properties.mail.debug=false spring.mail.test-connection=false jwt.secret=${JWT_SECRET_KEY:IPMaQEBhefMwnNhzvK9taBqokCl0KGNu+qTRiKsvmGU=} -jwt.expiration-ms=1800000 -jwt.refresh-expiration-ms=604800000 +jwt.expiration-ms=${JWT_EXPIRATION_MS:1800000} +jwt.refresh-expiration-ms=${JWT_REFRESH_EXPIRATION_MS:604800000} # Cloudinary configuration for testing cloudinary.cloud_name=${CLOUDINARY_CLOUD_NAME} diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index d13d496e..0fc621b4 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,67 +1,77 @@ --- Insert users -INSERT INTO users (username, name, email, password, role) VALUES -('disabled_user', 'Disabled User', 'disabledUser@sleepup.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER'), -('admin', 'Administrator', 'admin@sleepup.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'ADMIN'), -('user1', 'User One', 'user1@sleepup.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER'), -('carlos_garcia', 'Carlos García', 'carlos@gmail.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER'), -('maria_lopez', 'María López', 'maria@example.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER'), -('jose_martinez', 'José Martínez', 'jose@example.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER'), -('ana_rodriguez', 'Ana Rodríguez', 'ana@example.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER'), -('david_sanchez', 'David Sánchez', 'david@example.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER'), -('laura_fernandez', 'Laura Fernández', 'laura@example.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER'); +-- Tabla de control para verificar si ya se inicializó +CREATE TABLE IF NOT EXISTS db_initialization ( + id INT PRIMARY KEY DEFAULT 1, + initialized_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CHECK (id = 1) +); --- Insert accommodations -INSERT INTO accommodations (name, price, guest_number, pet_friendly, location, description, image_url, check_in_time, check_out_time, available_from, available_to, managed_by_user_id) VALUES -('Casa Rural El Olivar', 85.00, 6, true, 'Valencia', 'Encantadora casa rural rodeada de olivares con vistas panorámicas. Ideal para familias que buscan tranquilidad.', 'https://example.com/images/casa-olivar.jpg', '15:00:00', '11:00:00', '2026-01-01', '2026-12-31', 3), -('Apartamento Centro Madrid', 120.00, 4, false, 'Madrid', 'Moderno apartamento en el corazón de Madrid, cerca de todas las atracciones turísticas.', 'https://example.com/images/apt-madrid.jpg', '16:00:00', '12:00:00', '2026-01-01', '2026-12-31', 3), -('Villa Costa Brava', 200.00, 8, true, 'Girona', 'Espectacular villa frente al mar con piscina privada y jardín. Perfecta para grupos grandes.', 'https://example.com/images/villa-brava.jpg', '17:00:00', '10:00:00', '2026-03-01', '2026-10-31', 4), -('Chalet Sierra Nevada', 150.00, 6, true, 'Granada', 'Acogedor chalet en la sierra con chimenea y vistas a las montañas. Ideal para esquiadores.', 'https://example.com/images/chalet-sierra.jpg', '14:00:00', '11:00:00', '2026-01-01', '2026-12-31', 4), -('Piso Playa Malagueta', 95.00, 4, false, 'Málaga', 'Apartamento a pie de playa con terraza y vistas al mar Mediterráneo.', 'https://example.com/images/piso-malaga.jpg', '15:00:00', '12:00:00', '2026-04-01', '2026-11-30', 5), -('Casa Asturiana Tradicional', 75.00, 5, true, 'Asturias', 'Casa tradicional asturiana con hórreo y huerto ecológico. Ambiente rural auténtico.', 'https://example.com/images/casa-asturias.jpg', '16:00:00', '11:00:00', '2026-01-01', '2026-12-31', 5), -('Loft Barcelona Eixample', 140.00, 3, false, 'Barcelona', 'Loft de diseño en el Eixample barcelonés, completamente equipado y con wifi de alta velocidad.', 'https://example.com/images/loft-bcn.jpg', '15:30:00', '11:30:00', '2026-02-01', '2026-12-31', 6), -('Cortijo Andaluz', 110.00, 7, true, 'Sevilla', 'Auténtico cortijo andaluz con patio típico, ideal para conocer la cultura del sur.', 'https://example.com/images/cortijo-sevilla.jpg', '14:30:00', '12:00:00', '2026-01-01', '2026-12-31', 6), -('Apartamento Bilbao Centro', 100.00, 4, false, 'Bilbao', 'Céntrico apartamento cerca del Museo Guggenheim y la zona de pintxos.', 'https://example.com/images/apt-bilbao.jpg', '16:00:00', '11:00:00', '2026-01-01', '2026-12-31', 7), -( 'Casa Canaria con Piscina', 130.00, 6, true, 'Las Palmas', 'Casa típica canaria con piscina y barbacoa, a 5 minutos de la playa.', 'https://example.com/images/casa-canarias.jpg', '15:00:00', '12:00:00', '2026-01-01', '2026-12-31', 7), -( 'Estudio Toledo Histórico', 65.00, 2, false, 'Toledo', 'Pequeño pero encantador estudio en el casco histórico de Toledo.', 'https://example.com/images/estudio-toledo.jpg', '17:00:00', '11:00:00', '2026-01-01', '2026-12-31', 8), -( 'Casa Rural Extremeña', 80.00, 8, true, 'Cáceres', 'Amplia casa rural en dehesa extremeña, perfecta para desconectar de la ciudad.', 'https://example.com/images/casa-extremadura.jpg', '14:00:00', '12:00:00', '2026-01-01', '2026-12-31', 8); +-- Solo ejecutar si NO existe el flag de inicialización +SET @is_initialized = (SELECT COUNT(*) FROM db_initialization); --- Insert reservations (for all users EXCEPT admin (id=1) and user1 (id=2)) --- Reservations for carlos_garcia (id=3) -INSERT INTO reservations (user_id, guest_number, accommodation_id, check_in_date, check_out_date, booking_status, email_sent, created_date) VALUES -(3, 4, 5, '2026-07-15', '2026-07-22', 'CONFIRMED', true, NOW()), -(3, 2, 11, '2026-09-10', '2026-09-13', 'PENDING', false, NOW()); +-- Insert users (solo si no está inicializado) +INSERT INTO users (username, name, email, password, role) +SELECT * FROM ( + SELECT 'disabled_user' as username, 'Disabled User' as name, 'disabledUser@sleepup.com' as email, '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2' as password, 'USER' as role UNION ALL + SELECT 'admin', 'Administrator', 'admin@sleepup.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'ADMIN' UNION ALL + SELECT 'user1', 'User One', 'user1@sleepup.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER' UNION ALL + SELECT 'carlos_garcia', 'Carlos García', 'carlos@gmail.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER' UNION ALL + SELECT 'maria_lopez', 'María López', 'maria@example.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER' UNION ALL + SELECT 'jose_martinez', 'José Martínez', 'jose@example.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER' UNION ALL + SELECT 'ana_rodriguez', 'Ana Rodríguez', 'ana@example.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER' UNION ALL + SELECT 'david_sanchez', 'David Sánchez', 'david@example.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER' UNION ALL + SELECT 'laura_fernandez', 'Laura Fernández', 'laura@example.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER' +) AS tmp +WHERE @is_initialized = 0; --- Reservations for maria_lopez (id=4) -INSERT INTO reservations (user_id, guest_number, accommodation_id, check_in_date, check_out_date, booking_status, email_sent, created_date) VALUES -(4, 6, 1, '2026-08-05', '2026-08-12', 'CONFIRMED', true, NOW()), -(4, 3, 7, '2026-06-20', '2026-06-25', 'CANCELLED', true, NOW()), -(4, 4, 9, '2026-10-15', '2026-10-20', 'PENDING', false, NOW()); +-- Insert accommodations (solo si no está inicializado) +INSERT INTO accommodations (name, price, guest_number, pet_friendly, location, description, image_url, check_in_time, check_out_time, available_from, available_to, managed_by_user_id) +SELECT * FROM ( + SELECT 'Casa Rural El Olivar' as name, 85.00 as price, 6 as guest_number, true as pet_friendly, 'Valencia' as location, 'Encantadora casa rural rodeada de olivares con vistas panorámicas. Ideal para familias que buscan tranquilidad.' as description, 'https://example.com/images/casa-olivar.jpg' as image_url, '15:00:00' as check_in_time, '11:00:00' as check_out_time, '2026-01-01' as available_from, '2026-12-31' as available_to, 3 as managed_by_user_id UNION ALL + SELECT 'Apartamento Centro Madrid', 120.00, 4, false, 'Madrid', 'Moderno apartamento en el corazón de Madrid, cerca de todas las atracciones turísticas.', 'https://example.com/images/apt-madrid.jpg', '16:00:00', '12:00:00', '2026-01-01', '2026-12-31', 3 UNION ALL + SELECT 'Villa Costa Brava', 200.00, 8, true, 'Girona', 'Espectacular villa frente al mar con piscina privada y jardín. Perfecta para grupos grandes.', 'https://example.com/images/villa-brava.jpg', '17:00:00', '10:00:00', '2026-03-01', '2026-10-31', 4 UNION ALL + SELECT 'Chalet Sierra Nevada', 150.00, 6, true, 'Granada', 'Acogedor chalet en la sierra con chimenea y vistas a las montañas. Ideal para esquiadores.', 'https://example.com/images/chalet-sierra.jpg', '14:00:00', '11:00:00', '2026-01-01', '2026-12-31', 4 UNION ALL + SELECT 'Piso Playa Malagueta', 95.00, 4, false, 'Málaga', 'Apartamento a pie de playa con terraza y vistas al mar Mediterráneo.', 'https://example.com/images/piso-malaga.jpg', '15:00:00', '12:00:00', '2026-04-01', '2026-11-30', 5 UNION ALL + SELECT 'Casa Asturiana Tradicional', 75.00, 5, true, 'Asturias', 'Casa tradicional asturiana con hórreo y huerto ecológico. Ambiente rural auténtico.', 'https://example.com/images/casa-asturias.jpg', '16:00:00', '11:00:00', '2026-01-01', '2026-12-31', 5 UNION ALL + SELECT 'Loft Barcelona Eixample', 140.00, 3, false, 'Barcelona', 'Loft de diseño en el Eixample barcelonés, completamente equipado y con wifi de alta velocidad.', 'https://example.com/images/loft-bcn.jpg', '15:30:00', '11:30:00', '2026-02-01', '2026-12-31', 6 UNION ALL + SELECT 'Cortijo Andaluz', 110.00, 7, true, 'Sevilla', 'Auténtico cortijo andaluz con patio típico, ideal para conocer la cultura del sur.', 'https://example.com/images/cortijo-sevilla.jpg', '14:30:00', '12:00:00', '2026-01-01', '2026-12-31', 6 UNION ALL + SELECT 'Apartamento Bilbao Centro', 100.00, 4, false, 'Bilbao', 'Céntrico apartamento cerca del Museo Guggenheim y la zona de pintxos.', 'https://example.com/images/apt-bilbao.jpg', '16:00:00', '11:00:00', '2026-01-01', '2026-12-31', 7 UNION ALL + SELECT 'Casa Canaria con Piscina', 130.00, 6, true, 'Las Palmas', 'Casa típica canaria con piscina y barbacoa, a 5 minutos de la playa.', 'https://example.com/images/casa-canarias.jpg', '15:00:00', '12:00:00', '2026-01-01', '2026-12-31', 7 UNION ALL + SELECT 'Estudio Toledo Histórico', 65.00, 2, false, 'Toledo', 'Pequeño pero encantador estudio en el casco histórico de Toledo.', 'https://example.com/images/estudio-toledo.jpg', '17:00:00', '11:00:00', '2026-01-01', '2026-12-31', 8 UNION ALL + SELECT 'Casa Rural Extremeña', 80.00, 8, true, 'Cáceres', 'Amplia casa rural en dehesa extremeña, perfecta para desconectar de la ciudad.', 'https://example.com/images/casa-extremadura.jpg', '14:00:00', '12:00:00', '2026-01-01', '2026-12-31', 8 +) AS tmp +WHERE @is_initialized = 0; --- Reservations for jose_martinez (id=5) -INSERT INTO reservations (user_id, guest_number, accommodation_id, check_in_date, check_out_date, booking_status, email_sent, created_date) VALUES -(5, 5, 8, '2026-05-10', '2026-05-17', 'CONFIRMED', true, NOW()), -(5, 2, 11, '2026-11-01', '2026-11-04', 'PENDING', false, NOW()); --- Reservations for ana_rodriguez (id=6) -INSERT INTO reservations (user_id, guest_number, accommodation_id, check_in_date, check_out_date, booking_status, email_sent, created_date) VALUES -(6, 7, 3, '2026-07-01', '2026-07-08', 'CONFIRMED', true, NOW()), -(6, 4, 10, '2026-12-20', '2026-12-27', 'PENDING', false, NOW()), -(6, 3, 7, '2026-04-15', '2026-04-18', 'CONFIRMED', true, NOW()); +-- Insert reservations (solo si no está inicializado) +INSERT INTO reservations (user_id, guest_number, accommodation_id, check_in_date, check_out_date, booking_status, email_sent, created_date, total_price) +SELECT * FROM ( + SELECT 3 as user_id, 4 as guest_number, 5 as accommodation_id, '2026-07-15' as check_in_date, '2026-07-22' as check_out_date, 'CONFIRMED' as booking_status, true as email_sent, NOW() as created_date, 700.00 as total_price UNION ALL + SELECT 3, 2, 11, '2026-09-10', '2026-09-13', 'PENDING', false, NOW(), 300.00 UNION ALL + SELECT 4, 6, 1, '2026-08-05', '2026-08-12', 'CONFIRMED', true, NOW(), 1200.00 UNION ALL + SELECT 4, 3, 7, '2026-06-20', '2026-06-25', 'CANCELLED', true, NOW(), 500.00 UNION ALL + SELECT 4, 4, 9, '2026-10-15', '2026-10-20', 'PENDING', false, NOW(), 650.00 UNION ALL + SELECT 5, 5, 8, '2026-05-10', '2026-05-17', 'CONFIRMED', true, NOW(), 875.00 UNION ALL + SELECT 5, 2, 11, '2026-11-01', '2026-11-04', 'PENDING', false, NOW(), 350.00 UNION ALL + SELECT 6, 7, 3, '2026-07-01', '2026-07-08', 'CONFIRMED', true, NOW(), 980.00 UNION ALL + SELECT 6, 4, 10, '2026-12-20', '2026-12-27', 'PENDING', false, NOW(), 720.00 UNION ALL + SELECT 6, 3, 7, '2026-04-15', '2026-04-18', 'CONFIRMED', true, NOW(), 400.00 UNION ALL + SELECT 7, 6, 4, '2026-02-15', '2026-02-22', 'CONFIRMED', true, NOW(), 1140.00 UNION ALL + SELECT 7, 4, 5, '2026-09-05', '2026-09-10', 'PENDING', false, NOW(), 600.00 UNION ALL + SELECT 7, 8, 12, '2026-03-20', '2026-03-27', 'CONFIRMED', true, NOW(), 1360.00 UNION ALL + SELECT 8, 3, 2, '2026-06-01', '2026-06-05', 'CONFIRMED', true, NOW(), 420.00 UNION ALL + SELECT 8, 5, 6, '2026-08-15', '2026-08-20', 'PENDING', false, NOW(), 700.00 UNION ALL + SELECT 8, 2, 11, '2026-04-10', '2026-04-13', 'CANCELLED', true, NOW(), 300.00 UNION ALL + SELECT 8, 6, 10, '2026-11-10', '2026-11-15', 'CONFIRMED', true, NOW(), 1000.00 +) AS tmp +WHERE @is_initialized = 0; --- Reservations for david_sanchez (id=7) -INSERT INTO reservations (user_id, guest_number, accommodation_id, check_in_date, check_out_date, booking_status, email_sent, created_date) VALUES -(7, 6, 4, '2026-02-15', '2026-02-22', 'CONFIRMED', true, NOW()), -(7, 4, 5, '2026-09-05', '2026-09-10', 'PENDING', false, NOW()), -(7, 8, 12, '2026-03-20', '2026-03-27', 'CONFIRMED', true, NOW()); --- Reservations for laura_fernandez (id=8) -INSERT INTO reservations (user_id, guest_number, accommodation_id, check_in_date, check_out_date, booking_status, email_sent, created_date) VALUES -(8, 3, 2, '2026-06-01', '2026-06-05', 'CONFIRMED', true, NOW()), -(8, 5, 6, '2026-08-15', '2026-08-20', 'PENDING', false, NOW()), -(8, 2, 11, '2026-04-10', '2026-04-13', 'CANCELLED', true, NOW()), -(8, 6, 10, '2026-11-10', '2026-11-15', 'CONFIRMED', true, NOW()); +-- Marcar como inicializado (solo si no estaba antes) +INSERT INTO db_initialization (id) +SELECT 1 WHERE @is_initialized = 0; --- Reset sequences to avoid conflicts with future IDs +-- Reset sequences solo si se insertaron datos +SET @reset_sequences = (SELECT COUNT(*) FROM db_initialization WHERE id = 1); ALTER TABLE users AUTO_INCREMENT = 9; ALTER TABLE accommodations AUTO_INCREMENT = 13; ALTER TABLE reservations AUTO_INCREMENT = 18; \ No newline at end of file diff --git a/src/main/resources/templates/ConfirmationGuest.html b/src/main/resources/templates/ConfirmationGuest.html index 4674c619..eb63303d 100644 --- a/src/main/resources/templates/ConfirmationGuest.html +++ b/src/main/resources/templates/ConfirmationGuest.html @@ -74,6 +74,13 @@

Hello, Guest!

Check-out Date: Check-out + +
  • Amount: Amount
  • + +
  • + Discount: Discount Amount +
  • +

    You can manage your booking or view more details by logging into your diff --git a/src/main/resources/templates/NotificationReservationOwner.html b/src/main/resources/templates/NotificationReservationOwner.html index e50aa766..dd1b1dc2 100644 --- a/src/main/resources/templates/NotificationReservationOwner.html +++ b/src/main/resources/templates/NotificationReservationOwner.html @@ -65,6 +65,7 @@

    Hello, Owner!

  • Guest Name: Guest Name
  • Check-in Date: Check-in
  • Check-out Date: Check-out
  • +
  • Amount: Amount
  • You can view the reservation details and manage your property by logging into your SleepUp dashboard. diff --git a/src/test/java/com/SleepUp/SU/accommodation/controller/AccommodationControllerIntegrationTest.java b/src/test/java/com/SleepUp/SU/accommodation/controller/AccommodationControllerIntegrationTest.java index 89fd5c9f..abbc9e1a 100644 --- a/src/test/java/com/SleepUp/SU/accommodation/controller/AccommodationControllerIntegrationTest.java +++ b/src/test/java/com/SleepUp/SU/accommodation/controller/AccommodationControllerIntegrationTest.java @@ -70,8 +70,6 @@ void setUp() { imageFileOld = (MockMultipartFile) AccommodationTestData.defaultAccommodationRequestBuilder().image(); - - existingAccommodationId = accommodationRepository.findByManagedBy_Id(savedUser.getId()) .getFirst() .getId(); @@ -106,6 +104,42 @@ void getAccommodations_withNoUser_shouldReturnListOfAccommodations() throws Exce } } + @Nested + class GetAccommodationById { + + @Test + void getAccommodationById_validUser_shouldReturnAccommodationResponseDetail() throws Exception { + mockMvc.perform(get(BASE_API_PATH+ "/{id}", existingAccommodationId) + .with(user(customUserDetails)) + .accept("application/json")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("Downtown Studio")) + .andExpect(jsonPath("$.price").value(220.0)) + .andExpect(jsonPath("$.guestNumber").value(1)) + .andExpect(jsonPath("$.petFriendly").value(false)) + .andExpect(jsonPath("$.location").value("Los Angeles")); + } + + @Test + void getAccommodationById_withNoUser_shouldReturnAccommodationResponseDetail() throws Exception { + mockMvc.perform(get(BASE_API_PATH+ "/{id}", existingAccommodationId) + .accept("application/json")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("Downtown Studio")) + .andExpect(jsonPath("$.price").value(220.0)) + .andExpect(jsonPath("$.guestNumber").value(1)) + .andExpect(jsonPath("$.petFriendly").value(false)) + .andExpect(jsonPath("$.location").value("Los Angeles")); + } + + @Test + void getAccommodationById_nonExistingId_shouldReturnNotFound() throws Exception { + mockMvc.perform(delete(BASE_API_PATH + "/{id}", 999999L) + .with(user(adminUserDetails))) + .andExpect(status().isNotFound()); + } + } + @Nested class CreateAccommodation { @@ -116,7 +150,7 @@ void createAccommodation_validRequest_shouldReturnAccommodationResponseDetail() when(cloudinaryService.uploadFile(any(), anyString())) .thenReturn(Map.of("secure_url", "http://example.com/updated-image.jpg")); - mockMvc.perform(multipart(BASE_API_PATH) + mockMvc.perform(multipart(BASE_API_PATH ) .file(imageFileOld) .param("name", request.name()) .param("price", request.price().toString()) @@ -128,8 +162,7 @@ void createAccommodation_validRequest_shouldReturnAccommodationResponseDetail() .param("checkOutTime", request.checkOutTime().toString()) .param("availableFrom", request.availableFrom().toString()) .param("availableTo", request.availableTo().toString()) - .with(user(customUserDetails)) - .with(csrf())) + .with(user(customUserDetails))) .andDo(print()) .andExpect(status().isCreated()) .andExpect(content().contentType("application/json")) @@ -149,12 +182,37 @@ void createAccommodation_validRequest_shouldReturnAccommodationResponseDetail() void createAccommodation_missingRequiredFields_shouldReturnBadRequest() throws Exception { mockMvc.perform(multipart(BASE_API_PATH) .file(imageFileOld) - .param("name", "") // Empty name - .param("price", "-10") // Invalid price (edge case) + .param("name", "") + .param("price", "-10") .with(user(customUserDetails)) - .with(csrf())) + ) .andExpect(status().isBadRequest()); } + + @Test + void createAccommodation_withNoUser_shouldThrowException() throws Exception { + var request = AccommodationTestData.defaultAccommodationRequestBuilder(); + + when(cloudinaryService.uploadFile(any(), anyString())) + .thenReturn(Map.of("secure_url", "http://example.com/updated-image.jpg")); + + mockMvc.perform(multipart(BASE_API_PATH ) + .file(imageFileOld) + .param("name", request.name()) + .param("price", request.price().toString()) + .param("guestNumber", Integer.toString(request.guestNumber())) + .param("petFriendly", Boolean.toString(request.petFriendly())) + .param("location", request.location()) + .param("description", request.description()) + .param("checkInTime", request.checkInTime().toString()) + .param("checkOutTime", request.checkOutTime().toString()) + .param("availableFrom", request.availableFrom().toString()) + .param("availableTo", request.availableTo().toString())) + .andDo(print()) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.message").value("Unauthorized. Full authentication is required to access this resource")); + + } } @Nested @@ -232,24 +290,54 @@ void updateAccommodation_nonExistingId_shouldReturnNotFound() throws Exception { class DeleteAccommodation { @Test - void deleteAccommodation_existingId_shouldReturnNoContent() throws Exception { + void deleteAccommodation_existingIdAndOwnerUser_shouldReturnNoContent() throws Exception { mockMvc.perform(delete(BASE_API_PATH + "/{id}", existingAccommodationId) .with(user(customUserDetails))) .andExpect(status().isNoContent()); } + @Test + void deleteAccommodation_existingIdAndAdmin_shouldReturnNoContent() throws Exception { + mockMvc.perform(delete(BASE_API_PATH + "/{id}", existingAccommodationId) + .with(user(adminUserDetails))) + .andExpect(status().isNoContent()); + } + @Test void deleteAccommodation_userWithoutPermission_nonExistingId_shouldReturnForbidden() throws Exception { mockMvc.perform(delete(BASE_API_PATH + "/{id}", 999999L) - .with(user(customUserDetails))) // Assuming this user lacks permission + .with(user(customUserDetails))) + .andExpect(status().isForbidden()); + } + + @Test + void deleteAccommodation_withNoUser_nonExistingId_shouldReturnForbidden() throws Exception { + mockMvc.perform(delete(BASE_API_PATH + "/{id}", 999999L)) + .andDo(print()) + .andExpect(status().isUnauthorized()); + } + + @Test + void deleteAccommodation_nonExistingIdAndOwnerUser_shouldReturnForbidden() throws Exception { + mockMvc.perform(delete(BASE_API_PATH + "/{id}", 999999L) + .with(user(customUserDetails))) + .andDo(print()) .andExpect(status().isForbidden()); } + @Test + void deleteAccommodation_nonExistingIdAndAdmin_shouldReturnNoContent() throws Exception { + mockMvc.perform(delete(BASE_API_PATH + "/{id}", 999999L) + .with(user(adminUserDetails))) + .andDo(print()) + .andExpect(status().isNotFound()); + } + @Test void deleteAccommodation_admin_nonExistingId_shouldReturnNotFound() throws Exception { mockMvc.perform(delete(BASE_API_PATH + "/{id}", 999999L) - .with(user(adminUserDetails)) - .with(csrf())) + .with(user(adminUserDetails))) + .andDo(print()) .andExpect(status().isNotFound()); } diff --git a/src/test/java/com/SleepUp/SU/accommodation/dto/AccommodationMapperImplNullTests.java b/src/test/java/com/SleepUp/SU/accommodation/dto/AccommodationMapperImplNullTests.java new file mode 100644 index 00000000..21a6a65e --- /dev/null +++ b/src/test/java/com/SleepUp/SU/accommodation/dto/AccommodationMapperImplNullTests.java @@ -0,0 +1,127 @@ +package com.SleepUp.SU.accommodation.dto; + +import com.SleepUp.SU.accommodation.entity.Accommodation; +import com.SleepUp.SU.user.entity.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class AccommodationMapperImplNullTests { + + private AccommodationMapperImpl mapper; + + @BeforeEach + void setup() { + mapper = new AccommodationMapperImpl(); + } + + @Test + void toSummary_givenNullAccommodation_shouldReturnNull() { + AccommodationResponseSummary result = mapper.toSummary(null); + assertNull(result, "Expected null when passing null Accommodation"); + } + + @Test + void toSummary_withNullPetFriendly_shouldNotThrowAndMapOtherFields() { + Accommodation accommodation = Accommodation.builder() + .id(1L) + .name("Test Name") + .price(100.0) + .guestNumber(2) + .petFriendly(null) + .location("Test Location") + .imageUrl("test-url") + .build(); + + AccommodationResponseSummary summary = mapper.toSummary(accommodation); + + assertNotNull(summary); + assertEquals(1L, summary.id()); + assertEquals("Test Name", summary.name()); + assertEquals(100.0, summary.price()); + assertEquals(2, summary.guestNumber()); + assertEquals(Boolean.FALSE, summary.petFriendly(), "petFriendly should default to false instead of null"); + assertEquals("Test Location", summary.location()); + assertEquals("test-url", summary.imageUrl()); + } + + @Test + void toEntity_givenNullAccommodationRequestAndUser_shouldReturnNull() { + Accommodation accommodation = mapper.toEntity(null, null); + assertNull(accommodation, "Expected null when both accommodationRequest and user are null"); + } + + @Test + void toEntity_givenNullAccommodationRequestWithUser_shouldMapUserAndDefaultFields() { + User user = new User(); + Accommodation accommodation = mapper.toEntity(null, user); + + assertNotNull(accommodation); + assertEquals(user, accommodation.getManagedBy(), "User should be set in accommodation"); + // other fields should be default (null/zero) + assertNull(accommodation.getName()); + assertNull(accommodation.getLocation()); + } + + @Test + void toDetail_givenNullAccommodation_shouldReturnNull() { + AccommodationResponseDetail detail = mapper.toDetail(null); + assertNull(detail, "Expected null when passing null Accommodation"); + } + + @Test + void toDetail_withNullPetFriendly_shouldNotThrowAndMapOtherFields() { + User user = new User(); + user.setName("Test User"); + + Accommodation accommodation = Accommodation.builder() + .id(1L) + .name("Test Name") + .price(100.0) + .guestNumber(2) + .petFriendly(null) + .location("Test Location") + .description("Test description") + .checkInTime(null) + .checkOutTime(null) + .availableFrom(null) + .availableTo(null) + .managedBy(user) + .imageUrl("test-url") + .build(); + + AccommodationResponseDetail detail = mapper.toDetail(accommodation); + + assertNotNull(detail); + assertEquals("Test User", detail.managedByUsername()); + assertEquals(Boolean.FALSE, detail.petFriendly(), "petFriendly should default to false instead of null"); + assertEquals("Test Name", detail.name()); + assertEquals(100.0, detail.price()); + assertEquals(2, detail.guestNumber()); + assertEquals("test-url", detail.imageUrl()); + } + + @Test + void accommodationManagedByName_whenAccommodationOrUserOrNameIsNull_shouldReturnNull() throws Exception { + assertNull(invokePrivateManagedByName(null), "Expected null for null accommodation"); + + Accommodation accommodation = Accommodation.builder().managedBy(null).build(); + assertNull(invokePrivateManagedByName(accommodation), "Expected null if managedBy is null"); + + User userWithNullName = new User(); + userWithNullName.setName(null); + accommodation = Accommodation.builder().managedBy(userWithNullName).build(); + assertNull(invokePrivateManagedByName(accommodation), "Expected null if managedBy name is null"); + + userWithNullName.setName("Valid Name"); + accommodation = Accommodation.builder().managedBy(userWithNullName).build(); + assertEquals("Valid Name", invokePrivateManagedByName(accommodation)); + } + + private String invokePrivateManagedByName(Accommodation accommodation) throws Exception { + var method = AccommodationMapperImpl.class.getDeclaredMethod("accommodationManagedByName", Accommodation.class); + method.setAccessible(true); + return (String) method.invoke(mapper, accommodation); + } +} diff --git a/src/test/java/com/SleepUp/SU/accommodation/filter/AccommodationFilterServiceTest.java b/src/test/java/com/SleepUp/SU/accommodation/filter/AccommodationFilterServiceTest.java index 0c44510c..28d21cce 100644 --- a/src/test/java/com/SleepUp/SU/accommodation/filter/AccommodationFilterServiceTest.java +++ b/src/test/java/com/SleepUp/SU/accommodation/filter/AccommodationFilterServiceTest.java @@ -9,6 +9,7 @@ import com.SleepUp.SU.accommodation.dto.AccommodationResponseSummary; import com.SleepUp.SU.accommodation.dto.FilterAccommodationDTO; import com.SleepUp.SU.exceptions.InvalidDateRangeException; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; @@ -34,11 +35,48 @@ class AccommodationFilterServiceTest { @InjectMocks private AccommodationFilterService accommodationFilterService; + private Pageable pageable; + + @BeforeEach + void setUp() { + pageable = PageRequest.of(0, 10); + } + + private FilterAccommodationDTO createFilter(LocalDate fromDate, LocalDate toDate) { + return FilterAccommodationDTO.builder() + .name("Hotel") + .description("Nice place") + .minPrice(50.0) + .maxPrice(200.0) + .guestNumber(2) + .location("New York") + .fromDate(fromDate) + .toDate(toDate) + .build(); + } + + private void mockSpecificationAndRepository(FilterAccommodationDTO filter, + List accommodations) { + Specification spec = mock(Specification.class); + Page accommodationPage = new PageImpl<>(accommodations, pageable, accommodations.size()); + when(accommodationSpecification.buildSpecification(filter)).thenReturn(spec); + when(accommodationRepository.findAll(spec, pageable)).thenReturn(accommodationPage); + } + @Test - void testGetAllFilteredAccommodationsWithPagination() { - FilterAccommodationDTO filter = new FilterAccommodationDTO("Hotel", "Nice place", 50.0, 200.0, 2, "New York", LocalDate.now(), LocalDate.now().plusDays(5)); - Pageable pageable = PageRequest.of(0, 10); - AccommodationResponseSummary expectedDto = new AccommodationResponseSummary( 1L, "Hotel ABC", 10.0, 1, true, "Park Av", "image.url"); + void getAllFilteredAccommodationsWithPagination_validFilter_shouldReturnPageOfAccommodationSummaries() { + FilterAccommodationDTO filter = createFilter(LocalDate.now(), LocalDate.now().plusDays(5)); + + AccommodationResponseSummary expectedDto = AccommodationResponseSummary.builder() + .id(1L) + .name("Hotel ABC") + .price(10.0) + .guestNumber(1) + .petFriendly(true) + .location("Park Av") + .imageUrl("image.url") + .build(); + Accommodation accommodation = Accommodation.builder() .id(1L) .name("Hotel ABC") @@ -47,28 +85,85 @@ void testGetAllFilteredAccommodationsWithPagination() { .location("Park Av") .imageUrl("image.jpg") .build(); - Specification spec = mock(Specification.class); - Page accommodationPage = new PageImpl<>(List.of(accommodation), pageable, 1); - when(accommodationSpecification.buildSpecification(filter)).thenReturn(spec); - when(accommodationRepository.findAll(spec, pageable)).thenReturn(accommodationPage); + mockSpecificationAndRepository(filter, List.of(accommodation)); when(accommodationMapper.toSummary(accommodation)).thenReturn(expectedDto); Page result = accommodationFilterService.getAllFilteredAccommodationsWithPagination(filter, pageable); verify(accommodationSpecification).buildSpecification(filter); - verify(accommodationRepository).findAll(spec, pageable); + verify(accommodationRepository).findAll( + (Specification) any(), eq(pageable)); verify(accommodationMapper).toSummary(accommodation); assertEquals(new PageImpl<>(List.of(expectedDto), pageable, 1), result); - assertEquals(expectedDto, result.getContent().getFirst()); + assertEquals(expectedDto, result.getContent().get(0)); assertEquals(1, result.getTotalElements()); } @Test - void testGetAllFilteredAccommodationsWithPagination_checkOutBeforeCheckIn() { - FilterAccommodationDTO filter = new FilterAccommodationDTO("Hotel", "Nice place", 50.0, 200.0, 2, "New York", LocalDate.now().plusDays(10), LocalDate.now().plusDays(5)); - Pageable pageable = PageRequest.of(0, 10); + void getAllFilteredAccommodationsWithPagination_nullDates_shouldSkipDateValidationAndReturnEmptyPage() { + FilterAccommodationDTO filter = createFilter(null, null); + + mockSpecificationAndRepository(filter, List.of()); + + Page result = accommodationFilterService.getAllFilteredAccommodationsWithPagination(filter, pageable); + + verify(accommodationSpecification).buildSpecification(filter); + verify(accommodationRepository).findAll( + (Specification) any(), eq(pageable)); + + assertTrue(result.isEmpty()); + } + + @Test + void getAllFilteredAccommodationsWithPagination_nullFromDate_shouldSkipDateValidationAndReturnEmptyPage() { + FilterAccommodationDTO filter = createFilter(null, LocalDate.now().plusDays(5)); + + mockSpecificationAndRepository(filter, List.of()); + + Page result = accommodationFilterService.getAllFilteredAccommodationsWithPagination(filter, pageable); + + verify(accommodationSpecification).buildSpecification(filter); + verify(accommodationRepository).findAll( + (Specification) any(), eq(pageable)); + + assertTrue(result.isEmpty()); + } + + @Test + void getAllFilteredAccommodationsWithPagination_nullToDate_shouldSkipDateValidationAndReturnEmptyPage() { + FilterAccommodationDTO filter = createFilter(LocalDate.now(), null); + + mockSpecificationAndRepository(filter, List.of()); + + Page result = accommodationFilterService.getAllFilteredAccommodationsWithPagination(filter, pageable); + + verify(accommodationSpecification).buildSpecification(filter); + verify(accommodationRepository).findAll( + (Specification) any(), eq(pageable)); + + assertTrue(result.isEmpty()); + } + + @Test + void getAllFilteredAccommodationsWithPagination_nonNullDates_shouldInvokeDateValidationAndReturnEmptyPage() { + FilterAccommodationDTO filter = createFilter(LocalDate.now(), LocalDate.now().plusDays(5)); + + mockSpecificationAndRepository(filter, List.of()); + + Page result = accommodationFilterService.getAllFilteredAccommodationsWithPagination(filter, pageable); + + verify(accommodationSpecification).buildSpecification(filter); + verify(accommodationRepository).findAll( + (Specification) any(), eq(pageable)); + + assertTrue(result.isEmpty()); + } + + @Test + void getAllFilteredAccommodationsWithPagination_checkOutBeforeCheckIn_shouldThrowInvalidDateRangeException() { + FilterAccommodationDTO filter = createFilter(LocalDate.now().plusDays(10), LocalDate.now().plusDays(5)); InvalidDateRangeException exception = assertThrows( InvalidDateRangeException.class, @@ -76,13 +171,11 @@ void testGetAllFilteredAccommodationsWithPagination_checkOutBeforeCheckIn() { ); assertEquals("Check-in date must be before check-out date", exception.getMessage()); - } @Test - void testGetAllFilteredAccommodationsWithPagination_checkInBeforeToday() { - FilterAccommodationDTO filter = new FilterAccommodationDTO("Hotel", "Nice place", 50.0, 200.0, 2, "New York", LocalDate.now().minusDays(1), LocalDate.now().plusDays(5)); - Pageable pageable = PageRequest.of(0, 10); + void getAllFilteredAccommodationsWithPagination_checkInBeforeToday_shouldThrowInvalidDateRangeException() { + FilterAccommodationDTO filter = createFilter(LocalDate.now().minusDays(1), LocalDate.now().plusDays(5)); InvalidDateRangeException exception = assertThrows( InvalidDateRangeException.class, @@ -90,6 +183,5 @@ void testGetAllFilteredAccommodationsWithPagination_checkInBeforeToday() { ); assertEquals("Check-in date cannot be in the past", exception.getMessage()); - } } diff --git a/src/test/java/com/SleepUp/SU/accommodation/filter/AccommodationSpecificationTest.java b/src/test/java/com/SleepUp/SU/accommodation/filter/AccommodationSpecificationTest.java index 17f996cc..35ebfb0c 100644 --- a/src/test/java/com/SleepUp/SU/accommodation/filter/AccommodationSpecificationTest.java +++ b/src/test/java/com/SleepUp/SU/accommodation/filter/AccommodationSpecificationTest.java @@ -22,6 +22,7 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) +@SuppressWarnings("unchecked") public class AccommodationSpecificationTest { @Mock private Root root; @@ -30,6 +31,7 @@ public class AccommodationSpecificationTest { @Mock private Predicate predicate; @Mock private Path stringPath; + @Mock private Path booleanPath; @Mock private Path doublePath; @Mock private Path intPath; @Mock private Path datePath; @@ -44,27 +46,39 @@ public class AccommodationSpecificationTest { @BeforeEach void setUp() { - filter = new FilterAccommodationDTO( - "Hotel", - "Luxury place", - 50.0, - 200.0, - 4, - "Berlin", - LocalDate.of(2025, 9, 15), - LocalDate.of(2025, 9, 25)); + filter = FilterAccommodationDTO.builder() + .name("Hotel") + .description("Luxury place") + .minPrice(50.0) + .maxPrice(200.0) + .guestNumber(4) + .location("Berlin") + .fromDate(LocalDate.of(2025, 9, 15)) + .toDate(LocalDate.of(2025, 9, 25)) + .petFriendly(true) + .build(); } + private void stubStringProperty(String propertyName) { when(root.get(propertyName)).thenReturn((Path) stringPath); when(criteriaBuilder.lower(stringPath)).thenReturn(stringPath); lenient().when(criteriaBuilder.like(any(Expression.class), anyString())).thenReturn(predicate); } + + private void stubBooleanProperty(String propertyName) { + when(root.get(propertyName)).thenReturn((Path) booleanPath); + lenient().when(criteriaBuilder.isTrue(booleanPath)).thenReturn(predicate); + lenient().when(criteriaBuilder.isFalse( booleanPath)).thenReturn(predicate); + } + + private void stubDateProperty(String propertyName) { when(root.get(propertyName)).thenReturn((Path) datePath); } + private void stubDoubleProperty(String propertyName) { when(root.get(propertyName)).thenReturn((Path) doublePath); } @@ -163,7 +177,6 @@ void testPriceBetween_bothNull() { @Test void testGuestNumber() { - @SuppressWarnings("unchecked") Path path = (Path) mock(Path.class); when(root.get("guestNumber")).thenReturn(path); Integer guestNumber = 2; @@ -268,6 +281,30 @@ void testNoBookingOverlap_withNullEndDate() { assertNull(result, "Expected null predicate when newEndDate is null"); } + @Test + void testPetFriendly_true() { + stubBooleanProperty("petFriendly"); + Specification spec = AccommodationSpecification.petFriendly(true); + Predicate result = spec.toPredicate(root, query, criteriaBuilder); + assertSame(predicate, result, "Expected same predicate instance returned from petFriendly(true)"); + verify(criteriaBuilder).isTrue(any(Expression.class)); + } + + @Test + void testPetFriendly_false() { + stubBooleanProperty("petFriendly"); + Specification spec = AccommodationSpecification.petFriendly(false); + Predicate result = spec.toPredicate(root, query, criteriaBuilder); + assertSame(predicate, result, "Expected same predicate instance returned from petFriendly(false)"); + verify(criteriaBuilder).isFalse(any(Expression.class)); + } + + @Test + void testPetFriendly_null() { + Specification spec = AccommodationSpecification.petFriendly(null); + assertNull(spec.toPredicate(root, query, criteriaBuilder), "Expected null predicate for petFriendly(null)"); + } + @Test void testBuildSpecification() { LocalDate newStartDate = filter.fromDate(); @@ -280,6 +317,7 @@ void testBuildSpecification() { stubStringProperty("location"); stubDateProperty("availableFrom"); stubDateProperty("availableTo"); + stubBooleanProperty("petFriendly"); Subquery subquery = mock(Subquery.class); Root reservationRoot = mock(Root.class); diff --git a/src/test/java/com/SleepUp/SU/accommodation/security/AccommodationAccessEvaluatorTest.java b/src/test/java/com/SleepUp/SU/accommodation/security/AccommodationAccessEvaluatorTest.java index a31a6361..f702e8cf 100644 --- a/src/test/java/com/SleepUp/SU/accommodation/security/AccommodationAccessEvaluatorTest.java +++ b/src/test/java/com/SleepUp/SU/accommodation/security/AccommodationAccessEvaluatorTest.java @@ -1,36 +1,36 @@ package com.SleepUp.SU.accommodation.security; -import com.SleepUp.SU.accommodation.repository.AccommodationRepository; -import org.junit.jupiter.api.BeforeEach; +import com.SleepUp.SU.accommodation.utils.AccommodationServiceHelper; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.access.AccessDeniedException; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +@ExtendWith(MockitoExtension.class) class AccommodationAccessEvaluatorTest { - private AccommodationRepository accommodationRepository; - private AccommodationAccessEvaluator accommodationAccessEvaluator; + @Mock + private AccommodationServiceHelper accommodationServiceHelper; - @BeforeEach - void setUp() { - accommodationRepository = Mockito.mock(AccommodationRepository.class); - accommodationAccessEvaluator = new AccommodationAccessEvaluator(accommodationRepository); - } + @InjectMocks + private AccommodationAccessEvaluator accommodationAccessEvaluator; @Test void testIsOwner_true() { Long accommodationId = 1L; Long userId = 2L; - when(accommodationRepository.existsByIdAndManagedBy_Id(accommodationId, userId)).thenReturn(true); + when(accommodationServiceHelper.isAccommodationOwnedByUser(accommodationId, userId)).thenReturn(true); boolean result = accommodationAccessEvaluator.isOwner(accommodationId, userId); assertTrue(result); - verify(accommodationRepository, times(1)).existsByIdAndManagedBy_Id(accommodationId, userId); + verify(accommodationServiceHelper, times(1)).isAccommodationOwnedByUser(accommodationId, userId); } @Test @@ -38,17 +38,17 @@ void testIsOwner_false() { Long accommodationId = 1L; Long userId = 2L; - when(accommodationRepository.existsByIdAndManagedBy_Id(accommodationId, userId)).thenReturn(false); + when(accommodationServiceHelper.isAccommodationOwnedByUser(accommodationId, userId)).thenReturn(false); AccessDeniedException exception = assertThrows(AccessDeniedException.class, () -> { accommodationAccessEvaluator.isOwner(accommodationId, userId); }); assertEquals( - "User ID 2 cannot access Accommodation ID 1. Only the owner is authorized to access this resource.", + "Cause: You are not the owner of this accommodation.", exception.getMessage() ); - verify(accommodationRepository, times(1)).existsByIdAndManagedBy_Id(accommodationId, userId); + verify(accommodationServiceHelper, times(1)).isAccommodationOwnedByUser(accommodationId, userId); } } diff --git a/src/test/java/com/SleepUp/SU/accommodation/service/AccommodationServiceImplTest.java b/src/test/java/com/SleepUp/SU/accommodation/service/AccommodationServiceImplTest.java index 89a04d5b..797e7ebb 100644 --- a/src/test/java/com/SleepUp/SU/accommodation/service/AccommodationServiceImplTest.java +++ b/src/test/java/com/SleepUp/SU/accommodation/service/AccommodationServiceImplTest.java @@ -4,6 +4,7 @@ import com.SleepUp.SU.accommodation.dto.AccommodationResponseDetail; import com.SleepUp.SU.accommodation.dto.AccommodationResponseSummary; import com.SleepUp.SU.accommodation.entity.Accommodation; +import com.SleepUp.SU.accommodation.exceptions.AccommodationAlreadyExistsByNameException; import com.SleepUp.SU.accommodation.exceptions.AccommodationNotFoundByIdException; import com.SleepUp.SU.accommodation.repository.AccommodationRepository; import com.SleepUp.SU.accommodation.dto.AccommodationMapper; @@ -214,11 +215,11 @@ void updateAccommodation_nameExists_shouldThrow() { AccommodationRequest updatedRequest = defaultUpdateRequestBuilder(); when(accommodationServiceHelper.getAccommodationEntityById(1L)).thenReturn(accommodation); - doThrow(new IllegalArgumentException("Name exists")).when(accommodationServiceHelper).validateAccommodationNameDoesNotExist(updatedRequest.name()); + doThrow(new AccommodationAlreadyExistsByNameException(updatedRequest.name())).when(accommodationServiceHelper).validateAccommodationNameDoesNotExist(updatedRequest.name()); assertThatThrownBy(() -> accommodationService.updateAccommodation(1L, updatedRequest)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Name exists"); + .isInstanceOf(AccommodationAlreadyExistsByNameException.class) + .hasMessageContaining( "Accommodation with name '" + updatedRequest.name() + "' already exists"); verify(accommodationServiceHelper).getAccommodationEntityById(1L); verify(accommodationServiceHelper).validateAccommodationNameDoesNotExist(updatedRequest.name()); diff --git a/src/test/java/com/SleepUp/SU/accommodation/utilis/AccommodationServiceHelperTest.java b/src/test/java/com/SleepUp/SU/accommodation/utilis/AccommodationServiceHelperTest.java new file mode 100644 index 00000000..044b0d78 --- /dev/null +++ b/src/test/java/com/SleepUp/SU/accommodation/utilis/AccommodationServiceHelperTest.java @@ -0,0 +1,171 @@ +package com.SleepUp.SU.accommodation.utilis; + +import com.SleepUp.SU.accommodation.dto.AccommodationRequest; +import com.SleepUp.SU.accommodation.entity.Accommodation; +import com.SleepUp.SU.accommodation.exceptions.AccommodationAlreadyExistsByNameException; +import com.SleepUp.SU.accommodation.exceptions.AccommodationNotFoundByIdException; +import com.SleepUp.SU.accommodation.repository.AccommodationRepository; +import com.SleepUp.SU.accommodation.utils.AccommodationServiceHelper; +import com.SleepUp.SU.cloudinary.CloudinaryService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; +import org.springframework.mock.web.MockMultipartFile; + +import java.io.IOException; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class AccommodationServiceHelperTest { + + @Mock + private AccommodationRepository accommodationRepository; + + @Mock + private CloudinaryService cloudinaryService; + + @InjectMocks + private AccommodationServiceHelper helper; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void validateAccommodationNameDoesNotExist_nameExists_shouldThrowException() { + String name = "ExistingName"; + when(accommodationRepository.existsByName(name)).thenReturn(true); + + AccommodationAlreadyExistsByNameException exception = assertThrows( + AccommodationAlreadyExistsByNameException.class, + () -> helper.validateAccommodationNameDoesNotExist(name)); + + assertEquals("Accommodation with name '" + name+ "' already exists", exception.getMessage()); + } + + @Test + void validateAccommodationNameDoesNotExist_nameNotExists_shouldNotThrow() { + when(accommodationRepository.existsByName(anyString())).thenReturn(false); + assertDoesNotThrow(() -> helper.validateAccommodationNameDoesNotExist("NewName")); + } + + @Test + void getAccommodationEntityById_found_shouldReturnAccommodation() { + Accommodation accommodation = new Accommodation(); + accommodation.setId(1L); + when(accommodationRepository.findById(1L)).thenReturn(Optional.of(accommodation)); + + Accommodation result = helper.getAccommodationEntityById(1L); + assertEquals(accommodation, result); + } + + @Test + void getAccommodationEntityById_notFound_shouldThrowException() { + when(accommodationRepository.findById(99L)).thenReturn(Optional.empty()); + + assertThrows(AccommodationNotFoundByIdException.class, () -> helper.getAccommodationEntityById(99L)); + } + + @Test + void cloudinaryManagement_withImage_shouldDeleteAndPostImage() throws IOException { + AccommodationRequest request = mock(AccommodationRequest.class); + Accommodation accommodation = Accommodation.builder() + .imageUrl("mockurl").build(); + + MockMultipartFile imageFile = new MockMultipartFile("image", "filename.jpg", + "image/jpeg", "dummy data".getBytes()); + + when(request.image()).thenReturn(imageFile); + + doNothing().when(cloudinaryService).deleteFile(anyString()); + when(cloudinaryService.uploadFile(any(), anyString())).thenReturn(Map.of("secure_url", "http://example.com/image.jpg")); + + helper.cloudinaryManagement(request, accommodation); + + verify(cloudinaryService).deleteFile(anyString()); + verify(cloudinaryService).uploadFile(any(), eq("accommodations")); + assertEquals("http://example.com/image.jpg", accommodation.getImageUrl()); + } + + + @Test + void cloudinaryManagement_withNullOrEmptyImage_shouldDoNothing() { + AccommodationRequest request = mock(AccommodationRequest.class); + Accommodation accommodation = new Accommodation(); + + when(request.image()).thenReturn(null); + helper.cloudinaryManagement(request, accommodation); + verifyNoInteractions(cloudinaryService); + + MockMultipartFile emptyFile = mock(MockMultipartFile.class); + when(request.image()).thenReturn(emptyFile); + when(emptyFile.isEmpty()).thenReturn(true); + helper.cloudinaryManagement(request, accommodation); + verifyNoMoreInteractions(cloudinaryService); + } + + @Test + void getPublicIdCloudinary_shouldReturnCorrectPublicId() { + String urlWithVersion = "http://res.cloudinary.com/demo/image/upload/v1234/accommodation1.jpg"; + String urlWithoutVersion = "http://res.cloudinary.com/demo/image/upload/accommodation2.png"; + + String publicIdWithVersion = AccommodationServiceHelper.getPublicIdCloudinary(urlWithVersion); + String publicIdWithoutVersion = AccommodationServiceHelper.getPublicIdCloudinary(urlWithoutVersion); + + assertEquals("accommodation1", publicIdWithVersion); + assertEquals("accommodation2", publicIdWithoutVersion); + } + + @Test + void postImageCloudinary_success_shouldSetImageUrl() throws Exception { + AccommodationRequest request = mock(AccommodationRequest.class); + Accommodation accommodation = new Accommodation(); + MockMultipartFile image = new MockMultipartFile("image", "file.jpg", "image/jpeg", "content".getBytes()); + when(request.image()).thenReturn(image); + + when(cloudinaryService.uploadFile(any(), anyString())).thenReturn(Map.of("secure_url", "http://example.com/image.jpg")); + + helper.postImageCloudinary(request, accommodation); + + assertEquals("http://example.com/image.jpg", accommodation.getImageUrl()); + } + + @Test + void postImageCloudinary_failure_shouldSetDefaultImageUrl() throws Exception { + AccommodationRequest request = mock(AccommodationRequest.class); + Accommodation accommodation = new Accommodation(); + MockMultipartFile image = new MockMultipartFile("image", "file.jpg", "image/jpeg", "content".getBytes()); + when(request.image()).thenReturn(image); + + when(cloudinaryService.uploadFile(any(), anyString())).thenThrow(new RuntimeException("Any exception")); + + helper.postImageCloudinary(request, accommodation); + + assertEquals("http://localhost:8080/images/LOGO.png", accommodation.getImageUrl()); + } + + @Test + void deleteImageCloudinary_success_shouldInvokeDeleteFile() throws IOException { + String url = "http://res.cloudinary.com/demo/image/upload/v1/sample.jpg"; + + doNothing().when(cloudinaryService).deleteFile("sample"); + + helper.deleteImageCloudinary(url); + + verify(cloudinaryService).deleteFile("sample"); + } + + @Test + void deleteImageCloudinary_failure_shouldThrowRuntimeException() throws IOException { + String url = "http://res.cloudinary.com/demo/image/upload/v1/sample.jpg"; + + doThrow(new IOException("Delete failed")).when(cloudinaryService).deleteFile("sample"); + + RuntimeException ex = assertThrows(RuntimeException.class, () -> helper.deleteImageCloudinary(url)); + assertTrue(ex.getMessage().contains("Error deleting image from Cloudinary")); + } +} diff --git a/src/test/java/com/SleepUp/SU/auth/AuthControllerTest.java b/src/test/java/com/SleepUp/SU/auth/AuthControllerTest.java index f9b8cb46..1e1f43c2 100644 --- a/src/test/java/com/SleepUp/SU/auth/AuthControllerTest.java +++ b/src/test/java/com/SleepUp/SU/auth/AuthControllerTest.java @@ -1,12 +1,13 @@ package com.SleepUp.SU.auth; import com.SleepUp.SU.auth.dto.*; +import com.SleepUp.SU.user.dto.UserRequest; import com.SleepUp.SU.user.entity.CustomUserDetails; import com.SleepUp.SU.user.entity.User; import com.SleepUp.SU.user.repository.UserRepository; -import com.SleepUp.SU.user.role.Role; import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; +import jakarta.mail.internet.MimeMessage; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -14,12 +15,17 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; + import static org.hamcrest.Matchers.hasLength; +import static org.mockito.Mockito.*; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -53,38 +59,45 @@ public class AuthControllerTest { @Autowired private BCryptPasswordEncoder passwordEncoder; + @MockitoBean + private JavaMailSender mailSender; + private CustomUserDetails principal; @BeforeEach void setUp() { - User newUser = User.builder() - .name("New name") - .email("email@gmail.com") - .role(Role.USER) - .username("newUsername") - .password(passwordEncoder.encode("password123")) - .build(); - User savedUser = userRepository.save(newUser); + User savedUser = userRepository.findByUsername("User2") + .orElseThrow(() -> new RuntimeException("User2 not found")); principal = new CustomUserDetails(savedUser); } @Nested class RegisterTests { -// @Test -// void whenRegister_withValidData_shouldReturnCreatedUser() throws Exception { -// UserRequest request = new UserRequest("newUser", "New Name", "new@email.com", "password123"); -// -// mockMvc.perform(post(REGISTER_PATH) -// .contentType(MediaType.APPLICATION_JSON) -// .content(objectMapper.writeValueAsString(request))) -// .andExpect(status().isCreated()) -// .andExpect(jsonPath("$.username").value("newUser")) -// .andExpect(jsonPath("$.email").value("new@email.com")) -// .andExpect(jsonPath("$.name").value("New Name")); -// } @Test - void whenRegister_withInvalidData_shouldReturnBadRequest() throws Exception { + void register_validRequest_shouldReturnCreatedUser() throws Exception { + JavaMailSenderImpl javaMailSenderImpl = new JavaMailSenderImpl(); + MimeMessage mimeMessage = javaMailSenderImpl.createMimeMessage(); + + when(mailSender.createMimeMessage()).thenReturn(mimeMessage); + doNothing().when(mailSender).send(any(MimeMessage.class)); + + UserRequest request = new UserRequest("newUser", "New Name", "new@email.com", "password123"); + + mockMvc.perform(post(REGISTER_PATH) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.username").value("newUser")) + .andExpect(jsonPath("$.email").value("new@email.com")) + .andExpect(jsonPath("$.name").value("New Name")); + + verify(mailSender, times(1)).send(any(MimeMessage.class)); + } + + + @Test + void register_invalidRequest_shouldReturnBadRequest() throws Exception { String invalidJson = """ { "username": "", @@ -103,22 +116,22 @@ void whenRegister_withInvalidData_shouldReturnBadRequest() throws Exception { @Nested class LoginTests { @Test - void whenLogin_withValidCredentials_shouldReturnAuthResponse() throws Exception { + void login_validCredentials_shouldReturnAuthResponse() throws Exception { LoginRequest request = new LoginRequest(principal.getUsername(), "password123"); mockMvc.perform(post(LOGIN_PATH) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()) - .andExpect(jsonPath("$.token", hasLength(168))) - .andExpect(jsonPath("$.refreshToken", hasLength(168))); + .andExpect(jsonPath("$.token", hasLength(220))) + .andExpect(jsonPath("$.refreshToken", hasLength(220))); } } @Nested class RefreshTests { @Test - void whenRefresh_withAuthentication_shouldReturnNewTokens() throws Exception { + void refresh_withAuthentication_shouldReturnNewTokens() throws Exception { LoginRequest loginRequest = new LoginRequest(principal.getUsername(), "password123"); @@ -138,17 +151,16 @@ void whenRefresh_withAuthentication_shouldReturnNewTokens() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(refreshRequest))) .andExpect(status().isOk()) - .andExpect(jsonPath("$.token", hasLength(168))) - .andExpect(jsonPath("$.refreshToken", hasLength(168))); -// .andExpect(jsonPath("$.token.length()").value(168)) -// .andExpect(jsonPath("$.token").value(not(oldToken))) -// .andExpect(jsonPath("$.refreshToken.length()").value(168)) -// .andExpect(jsonPath("$.refreshToken").value(not(oldRefreshToken))); + .andExpect(jsonPath("$.token", hasLength(220))) + .andExpect(jsonPath("$.refreshToken", hasLength(220))) + .andExpect(jsonPath("$.token", org.hamcrest.Matchers.not(oldToken))) + .andExpect(jsonPath("$.refreshToken", org.hamcrest.Matchers.not(oldRefreshToken))); + } @Test - void whenRefresh_withoutAuthentication_shouldReturnUnauthorized() throws Exception { + void refresh_withoutAuthentication_shouldReturnUnauthorized() throws Exception { RefreshRequest request = new RefreshRequest("someRefreshToken"); mockMvc.perform(post(REFRESH_PATH) @@ -162,8 +174,7 @@ void whenRefresh_withoutAuthentication_shouldReturnUnauthorized() throws Excepti class LogoutTests { @Test - void whenLogout_withAuthentication_shouldReturnMessage() throws Exception { - // First, get a valid token by logging in (or reuse one from your test context) + void logout_withAuthentication_shouldReturnMessage() throws Exception { LoginRequest loginRequest = new LoginRequest(principal.getUsername(), "password123"); String loginResponse = mockMvc.perform(post(LOGIN_PATH) @@ -171,12 +182,12 @@ void whenLogout_withAuthentication_shouldReturnMessage() throws Exception { .content(objectMapper.writeValueAsString(loginRequest))) .andExpect(status().isOk()) .andReturn() + .getResponse() .getContentAsString(); String token = JsonPath.read(loginResponse, "$.token"); - // Use the token in the Authorization header for the logout request mockMvc.perform(post(LOGOUT_PATH) .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON)) diff --git a/src/test/java/com/SleepUp/SU/auth/AuthServiceTest.java b/src/test/java/com/SleepUp/SU/auth/AuthServiceTest.java index 123c3a91..cafbe5d6 100644 --- a/src/test/java/com/SleepUp/SU/auth/AuthServiceTest.java +++ b/src/test/java/com/SleepUp/SU/auth/AuthServiceTest.java @@ -12,7 +12,7 @@ import com.SleepUp.SU.user.dto.UserRequest; import com.SleepUp.SU.user.dto.UserResponse; import com.SleepUp.SU.user.role.Role; -import com.SleepUp.SU.utils.ApiMessageDto; +import com.SleepUp.SU.utils.dto.ApiMessageDto; import com.SleepUp.SU.utils.email.EmailServiceHelper; import jakarta.mail.MessagingException; import jakarta.servlet.http.HttpServletRequest; @@ -71,17 +71,9 @@ public class AuthServiceTest { class RegisterNewUserTest { @Test - void should_registerNewUser_fromRequest() throws MessagingException { + void register_validRequest_shouldReturnUserResponse() throws MessagingException { UserRequest userRequest = new UserRequest("userTest", "nameTest", "usertest@test.com", "password123"); - User mappedUser = new User(); - mappedUser.setUsername(userRequest.username()); - mappedUser.setEmail(userRequest.email()); - mappedUser.setName(userRequest.name()); - mappedUser.setPassword(userRequest.password()); - - doNothing().when(userServiceHelper).validateUserDoesNotExist(userRequest.username(), userRequest.email()); - User userSaved = new User(); userSaved.setId(1L); userSaved.setUsername("userTest"); @@ -90,7 +82,7 @@ void should_registerNewUser_fromRequest() throws MessagingException { userSaved.setPassword("password123"); userSaved.setRole(Role.USER); - when(userRepository.save(any(User.class))).thenReturn(userSaved); + when(userServiceHelper.createUser(userRequest, Role.USER)).thenReturn(userSaved); UserResponse userResponseMock = new UserResponse( userSaved.getId(), @@ -100,65 +92,21 @@ void should_registerNewUser_fromRequest() throws MessagingException { userSaved.getRole() ); when(userMapper.toResponse(userSaved)).thenReturn(userResponseMock); - when(passwordEncoder.encode(userRequest.password())).thenReturn("encondePassword"); - when(userMapper.toEntity(userRequest, "encondePassword", Role.USER)).thenReturn(mappedUser); UserResponse userResponse = authService.register(userRequest); assertEquals("userTest", userResponse.username()); assertEquals("usertest@test.com", userResponse.email()); - verify(emailService).sendWelcomeEmail(userRequest, mappedUser); + verify(emailService).sendWelcomeEmail(userSaved); } - @Test - void should_registerNewUser_throw_exceptionUsername(){ - UserRequest userRequest = new UserRequest("userTest", "nameTest", "usertest@test.com", "password123"); - - doThrow(new RuntimeException("UsernameAlreadyExistException")) - .when(userServiceHelper).validateUserDoesNotExist(userRequest.username(), userRequest.email()); - - RuntimeException exception = assertThrows(RuntimeException.class, () -> authService.register(userRequest)); - assertEquals("UsernameAlreadyExistException", exception.getMessage()); - } - - @Test - void should_registerNewUser_throw_exceptionEmail(){ - UserRequest userRequest = new UserRequest("userTest2", "nameTest", "usertest@test.com", "password123"); - - doThrow(new RuntimeException("EmailAlreadyExistException")) - .when(userServiceHelper).validateUserDoesNotExist(userRequest.username(), userRequest.email()); - - RuntimeException exception = assertThrows(RuntimeException.class, () -> authService.register(userRequest)); - assertEquals("EmailAlreadyExistException", exception.getMessage()); - } - - @Test - void should_RegisterNewUser_throw_dataIntegrityViolationException() { - UserRequest userRequest = new UserRequest("userTest", "nameTest", "usertest@test.com", "password123"); - - User mappedUser = new User(); - mappedUser.setUsername(userRequest.username()); - mappedUser.setEmail(userRequest.email()); - mappedUser.setName(userRequest.name()); - mappedUser.setPassword("encodedPassword"); - - doNothing().when(userServiceHelper).validateUserDoesNotExist(userRequest.username(), userRequest.email()); - when(passwordEncoder.encode(userRequest.password())).thenReturn("encodedPassword"); - when(userMapper.toEntity(userRequest, "encodedPassword", Role.USER)).thenReturn(mappedUser); - when(userRepository.save(any(User.class))) - .thenThrow(new DataIntegrityViolationException("Username or email already exists")); - - DataIntegrityViolationException exception = assertThrows(DataIntegrityViolationException.class, - () -> authService.register(userRequest)); - assertEquals("Username or email already exists", exception.getMessage()); - } } @Nested class LoginTest { @Test - void should_loginUser_successfully() { + void login_validCredentials_shouldReturnAuthResponse() { LoginRequest loginRequest = new LoginRequest("userTest", "password123"); Authentication mockAuthentication = mock(Authentication.class); @@ -180,7 +128,7 @@ void should_loginUser_successfully() { } @Test - void should_loginUser_throw_authenticationException() { + void login_invalidCredentials_shouldThrowAuthenticationException() { LoginRequest loginRequest = new LoginRequest("userTest", "wrongPassword"); when(authenticationManager.authenticate(any(UsernamePasswordAuthenticationToken.class))) @@ -198,7 +146,7 @@ void should_loginUser_throw_authenticationException() { class RefreshTest { @Test - void should_refreshToken_successfully() { + void refresh_validToken_shouldReturnAuthResponse() { RefreshRequest refreshRequest = new RefreshRequest("old-refresh-token"); CustomUserDetails mockCustomUserDetails = mock(CustomUserDetails.class); @@ -220,7 +168,7 @@ void should_refreshToken_successfully() { } @Test - void should_refreshToken_throw_exception_when_token_is_blacklisted() { + void refresh_blacklistedToken_shouldThrowAuthenticationException() { RefreshRequest refreshRequest = new RefreshRequest("blacklisted-token"); CustomUserDetails mockCustomUserDetails = mock(CustomUserDetails.class); @@ -235,7 +183,7 @@ void should_refreshToken_throw_exception_when_token_is_blacklisted() { } @Test - void should_refreshToken_throw_exception_when_token_is_invalid() { + void refresh_invalidToken_shouldThrowAuthenticationException() { RefreshRequest refreshRequest = new RefreshRequest("invalid-token"); CustomUserDetails mockCustomUserDetails = mock(CustomUserDetails.class); @@ -255,7 +203,7 @@ void should_refreshToken_throw_exception_when_token_is_invalid() { class LogoutTest { @Test - void should_logout_successfully_with_access_token_only() { + void logout_withAccessTokenOnly_shouldReturnApiMessage() { HttpServletRequest mockRequest = mock(HttpServletRequest.class); when(mockRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer access-token"); @@ -271,7 +219,7 @@ void should_logout_successfully_with_access_token_only() { } @Test - void should_logout_successfully_with_both_tokens() { + void logout_withBothTokens_shouldReturnApiMessage() { HttpServletRequest mockRequest = mock(HttpServletRequest.class); when(mockRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer access-token"); @@ -288,7 +236,7 @@ void should_logout_successfully_with_both_tokens() { } @Test - void should_logout_throw_exception_when_no_authorization_header() { + void logout_noAuthorizationHeader_shouldThrowAuthenticationException() { HttpServletRequest mockRequest = mock(HttpServletRequest.class); when(mockRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(null); @@ -301,7 +249,7 @@ void should_logout_throw_exception_when_no_authorization_header() { } @Test - void should_logout_throw_exception_when_invalid_authorization_header() { + void logout_invalidAuthorizationHeaderFormat_shouldThrowAuthenticationException() { HttpServletRequest mockRequest = mock(HttpServletRequest.class); when(mockRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Invalid-header"); @@ -314,7 +262,7 @@ void should_logout_throw_exception_when_invalid_authorization_header() { } @Test - void should_logout_successfully_with_invalid_access_token() { + void logout_invalidAccessToken_shouldReturnApiMessageWithoutBlacklisting() { HttpServletRequest mockRequest = mock(HttpServletRequest.class); when(mockRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer invalid-token"); @@ -328,7 +276,7 @@ void should_logout_successfully_with_invalid_access_token() { } @Test - void should_logout_successfully_with_valid_access_token_and_invalid_refresh_token() { + void logout_validAccessTokenAndInvalidRefreshToken_shouldBlacklistAccessOnly() { HttpServletRequest mockRequest = mock(HttpServletRequest.class); when(mockRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer access-token"); diff --git a/src/test/java/com/SleepUp/SU/auth/TokenBlacklistServiceTest.java b/src/test/java/com/SleepUp/SU/auth/TokenBlacklistServiceTest.java index 25c9939d..03630f4b 100644 --- a/src/test/java/com/SleepUp/SU/auth/TokenBlacklistServiceTest.java +++ b/src/test/java/com/SleepUp/SU/auth/TokenBlacklistServiceTest.java @@ -26,7 +26,7 @@ void setUp() { } @Test - void when_addTokenToBlacklist_withValidToken() { + void addToBlacklist_validToken_shouldBeInBlacklist() { String token = "valid-token"; Date expiration = new Date(System.currentTimeMillis() + 10000); when(jwtService.extractExpiration(token)).thenReturn(expiration); @@ -37,7 +37,7 @@ void when_addTokenToBlacklist_withValidToken() { } @Test - void when_addTokenToBlacklist_withInvalidToken_throw_exception() { + void addToBlacklist_invalidToken_shouldStillBeInBlacklist() { String token = "invalid-token"; when(jwtService.extractExpiration(token)).thenThrow(new RuntimeException("Invalid token")); @@ -47,12 +47,12 @@ void when_addTokenToBlacklist_withInvalidToken_throw_exception() { } @Test - void when_returnFalse_when_tokenNotInBlacklist() { + void isTokenInBlacklist_tokenNotInBlacklist_shouldReturnFalse() { assertFalse(tokenBlacklistService.isTokenInBlacklist("not-exists-token")); } @Test - void when_returnFalseAndRemoveToken_when_tokenExpired() { + void isTokenInBlacklist_expiredToken_shouldReturnFalseAndRemoveToken() { String token = "expired-token"; Date expiration = new Date(System.currentTimeMillis() - 1000); when(jwtService.extractExpiration(token)).thenReturn(expiration); @@ -63,7 +63,7 @@ void when_returnFalseAndRemoveToken_when_tokenExpired() { } @Test - void when_removeTokenFromBlacklist() { + void removeFromBlacklist_existingToken_shouldRemoveToken() { String token = "remove-token"; Date expiration = new Date(System.currentTimeMillis() + 10000); when(jwtService.extractExpiration(token)).thenReturn(expiration); @@ -77,7 +77,7 @@ void when_removeTokenFromBlacklist() { } @Test - void when_removeExpiredTokens_scheduledTask() { + void removeExpiredTokens_taskExecuted_shouldRemoveExpiredAndKeepActiveTokens() { String expiredToken = "expired-token"; String activeToken = "active-token"; @@ -97,7 +97,7 @@ void when_removeExpiredTokens_scheduledTask() { } @Test - void when_returnCorrectBlacklistedTokensCount() { + void getBlacklistedTokensCount_multipleActiveTokens_shouldReturnCorrectCount() { String token1 = "token1"; String token2 = "token2"; Date expiration = new Date(System.currentTimeMillis() + 10000); diff --git a/src/test/java/com/SleepUp/SU/cloudinary/CloudinaryServiceTest.java b/src/test/java/com/SleepUp/SU/cloudinary/CloudinaryServiceTest.java index f2345007..85ce146b 100644 --- a/src/test/java/com/SleepUp/SU/cloudinary/CloudinaryServiceTest.java +++ b/src/test/java/com/SleepUp/SU/cloudinary/CloudinaryServiceTest.java @@ -38,8 +38,8 @@ void setUp() { class uploadFile { @Test - void uploadFile_success() throws IOException { - MockMultipartFile image = new MockMultipartFile( + void uploadFile_validFileWithFolder_shouldReturnSecureUrl() throws IOException { + MockMultipartFile image = new MockMultipartFile( "image", "test-image.jpg", "image/jpeg", @@ -56,13 +56,32 @@ void uploadFile_success() throws IOException { assertEquals("http://cloudinary.com/image/upload/example.jpg", result.get("secure_url")); } + + @Test + void uploadFile_nullFolder_shouldReturnSecureUrl() throws IOException { + MockMultipartFile image = new MockMultipartFile( + "image", + "test-image.jpg", + "image/jpeg", + "Test Image Content".getBytes() + ); + + Map response = new HashMap<>(); + response.put("secure_url", "http://cloudinary.com/image/upload/example-null-folder.jpg"); + + Mockito.when(cloudinary.uploader()).thenReturn(uploader); + Mockito.when(uploader.upload(any(), anyMap())).thenReturn(response); + + Map result = cloudinaryService.uploadFile(image, null); + + assertEquals("http://cloudinary.com/image/upload/example-null-folder.jpg", result.get("secure_url")); + } } @Nested class deleteFile { @Test - void deleteFile_success() throws IOException { - + void deleteFile_validPublicId_shouldNotThrow() throws IOException { Map response = new HashMap<>(); response.put("result", "ok"); @@ -72,4 +91,4 @@ void deleteFile_success() throws IOException { assertDoesNotThrow(() -> cloudinaryService.deleteFile("image")); } } -} \ No newline at end of file +} diff --git a/src/test/java/com/SleepUp/SU/config/StartupValidationRunnerTest.java b/src/test/java/com/SleepUp/SU/config/StartupValidationRunnerTest.java new file mode 100644 index 00000000..eb24264f --- /dev/null +++ b/src/test/java/com/SleepUp/SU/config/StartupValidationRunnerTest.java @@ -0,0 +1,143 @@ +package com.SleepUp.SU.config; + +import com.SleepUp.SU.config.properties.DatasourceProperties; +import com.cloudinary.Cloudinary; +import com.cloudinary.Api; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.boot.ApplicationArguments; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class StartupValidationRunnerTest { + + @Mock + private DataSource dataSource; + + @Mock + private DatasourceProperties datasourceProperties; + + @Mock + private Cloudinary cloudinary; + + @Mock + private Api cloudinaryApi; + + @Mock + private Connection connection; + + private StartupValidationRunner runner; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + runner = new StartupValidationRunner(dataSource, datasourceProperties, cloudinary); + } + + @Nested + class RunTests { + + @Test + void run_withValidConnections_shouldCompleteSuccessfully() throws Exception { + when(datasourceProperties.getUrl()).thenReturn("jdbc:testdb"); + when(dataSource.getConnection()).thenReturn(connection); + when(connection.isClosed()).thenReturn(false); + when(cloudinary.api()).thenReturn(cloudinaryApi); + doReturn(null).when(cloudinaryApi).usage(anyMap()); + + runner.run(mock(ApplicationArguments.class)); + + verify(dataSource).getConnection(); + verify(cloudinary).api(); + verify(cloudinaryApi).usage(anyMap()); + } + + @Test + void run_withCloudinaryFailure_shouldThrowRuntimeException() throws Exception { + when(datasourceProperties.getUrl()).thenReturn("jdbc:testdb"); + when(dataSource.getConnection()).thenReturn(connection); + when(connection.isClosed()).thenReturn(false); + when(cloudinary.api()).thenReturn(cloudinaryApi); + doThrow(new RuntimeException("Cloudinary down")).when(cloudinaryApi).usage(anyMap()); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> + runner.run(mock(ApplicationArguments.class)) + ); + + assertTrue(exception.getMessage().contains("Cloudinary connection validation failed")); + } + } + + @Nested + class ValidateDatabaseConnectionTests { + + @Test + void validateDatabaseConnection_withNullUrl_shouldThrowRuntimeException() { + when(datasourceProperties.getUrl()).thenReturn(null); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + runner.run(mock(ApplicationArguments.class)); + }); + + assertEquals("DB URL is invalid or missing", exception.getMessage()); + } + + @Test + void validateDatabaseConnection_withEmptyUrl_shouldThrowRuntimeException() { + when(datasourceProperties.getUrl()).thenReturn(""); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + runner.run(mock(ApplicationArguments.class)); + }); + + assertEquals("DB URL is invalid or missing", exception.getMessage()); + } + + @Test + void validateDatabaseConnection_withNullConnection_shouldThrowRuntimeException() throws SQLException { + when(datasourceProperties.getUrl()).thenReturn("jdbc:testdb"); + when(dataSource.getConnection()).thenReturn(null); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + runner.run(mock(ApplicationArguments.class)); + }); + + assertEquals("Database connection not available", exception.getMessage()); + } + + @Test + void validateDatabaseConnection_withClosedConnection_shouldThrowRuntimeException() throws SQLException { + when(datasourceProperties.getUrl()).thenReturn("jdbc:testdb"); + when(dataSource.getConnection()).thenReturn(connection); + when(connection.isClosed()).thenReturn(true); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + runner.run(mock(ApplicationArguments.class)); + }); + + assertEquals("Database connection not available", exception.getMessage()); + verify(connection).close(); + } + + @Test + void validateDatabaseConnection_withSQLException_shouldThrowRuntimeException() throws SQLException { + when(datasourceProperties.getUrl()).thenReturn("jdbc:testdb"); + when(dataSource.getConnection()).thenThrow(new SQLException("DB down")); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + runner.run(mock(ApplicationArguments.class)); + }); + + assertTrue(exception.getMessage().contains("Failed to connect to DB")); + assertTrue(exception.getCause() instanceof SQLException); + } + } +} diff --git a/src/test/java/com/SleepUp/SU/exceptions/GlobalExceptionHandlerTest.java b/src/test/java/com/SleepUp/SU/exceptions/GlobalExceptionHandlerTest.java new file mode 100644 index 00000000..b76c830e --- /dev/null +++ b/src/test/java/com/SleepUp/SU/exceptions/GlobalExceptionHandlerTest.java @@ -0,0 +1,224 @@ +package com.SleepUp.SU.exceptions; + +import com.SleepUp.SU.accommodation.exceptions.AccommodationAlreadyExistsByNameException; +import com.SleepUp.SU.accommodation.exceptions.AccommodationNotFoundByIdException; +import com.SleepUp.SU.reservation.exceptions.*; +import com.SleepUp.SU.utils.exceptions.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Path; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class GlobalExceptionHandlerTest { + + private GlobalExceptionHandler handler; + private HttpServletRequest request; + + @BeforeEach + public void setup() { + handler = new GlobalExceptionHandler(); + request = mock(HttpServletRequest.class); + when(request.getRequestURI()).thenReturn("/test"); + } + + @Test + public void testHandleInvalidDateRangeException() { + InvalidDateRangeException ex = new InvalidDateRangeException(InvalidDateRangeError.ORDER); + ResponseEntity response = handler.handleInvalidDateRangeException(ex, request); + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertTrue(((String)response.getBody().message()).contains(InvalidDateRangeError.ORDER.getMessage())); + } + + @Test + public void testHandleUserNotFoundByIdException() { + UserNotFoundByIdException ex = new UserNotFoundByIdException(1L); + ResponseEntity response = handler.handleUserNotFoundByIdeException(ex, request); + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + } + + @Test + public void testHandleUserNotFoundByUsernameException() { + UserNotFoundByUsernameException ex = new UserNotFoundByUsernameException("User not found by username"); + ResponseEntity response = handler.handleUserNotFoundByUsernameException(ex, request); + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + } + + @Test + public void testHandleUserEmailAlreadyExists() { + UserEmailAlreadyExistsException ex = new UserEmailAlreadyExistsException("Email exists"); + ResponseEntity response = handler.handleUserEmailAlreadyExists(ex, request); + assertEquals(HttpStatus.CONFLICT, response.getStatusCode()); + } + + @Test + public void testHandleUserUsernameAlreadyExists() { + UserUsernameAlreadyExistsException ex = new UserUsernameAlreadyExistsException("Username exists"); + ResponseEntity response = handler.handleUserUsernameAlreadyExists(ex, request); + assertEquals(HttpStatus.CONFLICT, response.getStatusCode()); + } + + @Test + public void testHandleAccommodationNotFoundByIdException() { + AccommodationNotFoundByIdException ex = new AccommodationNotFoundByIdException(1L); + ResponseEntity response = handler.handleAccommodationNotFoundByIdException(ex, request); + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + } + + @Test + public void testHandleAccommodationAlreadyExistsByName() { + AccommodationAlreadyExistsByNameException ex = new AccommodationAlreadyExistsByNameException("Already exists"); + ResponseEntity response = handler.handleAccommodationAlreadyExistsByName(ex, request); + assertEquals(HttpStatus.CONFLICT, response.getStatusCode()); + } + + @Test + public void testHandleValidationExceptions() { + FieldError fieldError = new FieldError("obj", "field", "must not be blank"); + + BindingResult bindingResult = mock(BindingResult.class); + when(bindingResult.getAllErrors()).thenReturn(Collections.singletonList(fieldError)); + + MethodArgumentNotValidException ex = mock(MethodArgumentNotValidException.class); + when(ex.getBindingResult()).thenReturn(bindingResult); + + ResponseEntity response = handler.handleValidationExceptions(ex, request); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + Map errors = (Map) response.getBody().message(); + assertTrue(errors.containsKey("field")); + } + + @Test + public void testHandleConstraintViolationExceptions() { + Path path = mock(Path.class); + ConstraintViolation violation = mock(ConstraintViolation.class); + when(violation.getPropertyPath()).thenReturn(path); + when(violation.getMessage()).thenReturn("must not be null"); + + Set> violations = new HashSet<>(); + violations.add(violation); + ConstraintViolationException ex = new ConstraintViolationException(violations); + + ResponseEntity response = handler.handleConstraintViolationExceptions(ex, request); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + + @SuppressWarnings("unchecked") + Map errors = (Map) response.getBody().message(); + + assertNotNull(errors, "Errors map should not be null"); + assertFalse(errors.isEmpty(), "Errors map should not be empty"); + assertEquals(1, errors.size(), "Errors map size should be 1"); + + String errorKey = path.toString(); + assertTrue(errors.containsKey(errorKey), "Errors map should contain key: " + errorKey); + assertEquals("must not be null", errors.get(errorKey), "Error message should match"); + } + + @Test + public void testHandleHttpMessageNotReadableException() { + HttpMessageNotReadableException ex = new HttpMessageNotReadableException("Malformed JSON"); + ResponseEntity response = handler.handleEmptyOrMalformedBodyRequest(ex, request); + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals("Request body is required and cannot be empty or malformed.", response.getBody().message()); + } + + @Test + public void testHandleAccessDeniedException() { + AccessDeniedException ex = new AccessDeniedException("Access denied"); + ResponseEntity response = handler.handleAccessDeniedException(ex, request); + assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); + assertTrue(((String)response.getBody().message()).contains("Access denied")); + } + + @Test + public void testHandleBadCredentialsException() { + BadCredentialsException ex = new BadCredentialsException("Bad credentials"); + ResponseEntity response = handler.handleBadCredentialsException(ex, request); + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + assertTrue(((String)response.getBody().message()).contains("Unauthorized")); + } + + @Test + public void testHandleReservationNotFoundByIdException() { + ReservationNotFoundByIdException ex = new ReservationNotFoundByIdException(1L); + ResponseEntity response = handler.handleReservationNotFoundByIdeException(ex, request); + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + } + + @Test + public void testHandleAccommodationConstraintsException() { + AccommodationConstraintsException ex = new AccommodationConstraintsException("Constraint violated"); + ResponseEntity response = handler.handleAccommodationConstraintsException(ex, request); + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + } + + @Test + public void testHandleAccommodationUnavailableException() { + AccommodationUnavailableException ex = new AccommodationUnavailableException("Unavailable"); + ResponseEntity response = handler.handleAccommodationUnavailableException(ex, request); + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + } + + @Test + public void testHandleReservationOverlapException() { + ReservationOverlapException ex = new ReservationOverlapException("Overlap"); + ResponseEntity response = handler.handleReservationOverlapException(ex, request); + assertEquals(HttpStatus.CONFLICT, response.getStatusCode()); + } + + @Test + public void testHandleReservationModificationException() { + ReservationModificationException ex = new ReservationModificationException("Modification conflict"); + ResponseEntity response = handler.handleReservationModificationException(ex, request); + assertEquals(HttpStatus.CONFLICT, response.getStatusCode()); + } + + @Test + public void testHandleReservationAccommodationOwnerException() { + ReservationAccommodationOwnerException ex = new ReservationAccommodationOwnerException(); + ResponseEntity response = handler.handleReservationAccommodationOwnerException(ex, request); + assertEquals(HttpStatus.CONFLICT, response.getStatusCode()); + } + + @Test + public void testHandleAllUnhandledExceptions() { + Exception ex = new Exception("Unknown error"); + ResponseEntity response = handler.handleAllUnhandledExceptions(ex, request); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + Map errors = (Map) response.getBody().message(); + assertEquals("Unknown error", errors.get("error")); + } + + @Test + public void testHandleAllUnhandledExceptions_NoMessage() { + Exception ex = new Exception(); + ResponseEntity response = handler.handleAllUnhandledExceptions(ex, request); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + Map errors = (Map) response.getBody().message(); + assertTrue(errors.containsKey("error")); + assertEquals("Unexpected error", errors.get("error")); + + } +} diff --git a/src/test/java/com/SleepUp/SU/reservation/ReservationControllerTest.java b/src/test/java/com/SleepUp/SU/reservation/ReservationControllerTest.java index b8c5c773..3d8e73d2 100644 --- a/src/test/java/com/SleepUp/SU/reservation/ReservationControllerTest.java +++ b/src/test/java/com/SleepUp/SU/reservation/ReservationControllerTest.java @@ -114,29 +114,29 @@ void getMyReservations_unauthenticated_shouldReturnUnauthorized() throws Excepti @Nested class CreateReservationTests { - @Test - void createReservation_validData_shouldReturnCreatedReservation() throws Exception { - ReservationRequest request = new ReservationRequest( - 1, - LocalDate.of(2025, 10, 1), - LocalDate.of(2025, 10, 8) - ); - - Long accommodationId = 2L; - Accommodation accommodation = accommodationRepository.findById(accommodationId) - .orElseThrow(() -> new RuntimeException("Accommodation with id 2L not found")); - - mockMvc.perform(post(ACCOMMODATIONS_PATH + "/{accommodationId}/reservations", accommodationId) - .with(user(principal2)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andDo(print()) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.accommodationName").value(accommodation.getName())) - .andExpect(jsonPath("$.guestNumber").value(1)) - .andExpect(jsonPath("$.bookingStatus").value("PENDING")) - .andExpect(jsonPath("$.userName").value(principal2.getUser().getName())); - } +//@Test +//void createReservation_validData_shouldReturnCreatedReservation() throws Exception { +// ReservationRequest request = new ReservationRequest( +// 1, +// LocalDate.of(2025, 10, 1), +// LocalDate.of(2025, 10, 8) +// ); +// +// Long accommodationId = 2L; +// Accommodation accommodation = accommodationRepository.findById(accommodationId) +// .orElseThrow(() -> new RuntimeException("Accommodation with id 2L not found")); +// +// mockMvc.perform(post(ACCOMMODATIONS_PATH + "/{accommodationId}/reservations", accommodationId) +// .with(user(principal2)) +// .contentType(MediaType.APPLICATION_JSON) +// .content(objectMapper.writeValueAsString(request))) +// .andDo(print()) +// .andExpect(status().isCreated()) +// .andExpect(jsonPath("$.accommodationName").value(accommodation.getName())) +// .andExpect(jsonPath("$.guestNumber").value(1)) +// .andExpect(jsonPath("$.bookingStatus").value("PENDING")) +// .andExpect(jsonPath("$.userName").value(principal2.getUser().getName())); +//} @Test void createReservation_tooManyGuests_shouldReturnBadRequest() throws Exception { @@ -238,45 +238,42 @@ void createReservation_pastDates_shouldReturnBadRequest() throws Exception { .andExpect(status().isBadRequest()); } } - @Nested - class CancelReservationTests { + class DeleteReservationAdminTests { @Test - void cancelReservation_validRequest_shouldReturnCancelledReservation() throws Exception { - Long reservationId = 5L; + void deleteReservation_asAdmin_shouldReturnNoContent() throws Exception { + Long reservationId = 3L; - mockMvc.perform(patch(ACCOMMODATIONS_PATH + "/{id}/reservations/cancel", reservationId) - .with(user(principal)) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON)) + mockMvc.perform(delete(RESERVATIONS_PATH + "/admin/{id}", reservationId) + .with(user("admin").roles("ADMIN")) + .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)); + .andExpect(status().isNoContent()); } @Test - void cancelReservation_alreadyStarted_shouldReturnConflict() throws Exception { - Long reservationId = 1L; + void deleteReservation_nonAdmin_shouldReturnForbidden() throws Exception { + Long reservationId = 3L; - mockMvc.perform(patch(ACCOMMODATIONS_PATH + "/{id}/reservations/cancel", reservationId) - .with(user(principal)) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isConflict()) - .andExpect(jsonPath("$.message").value("Cannot modify a reservation that has already started")); + mockMvc.perform(delete(RESERVATIONS_PATH + "/admin/{id}", reservationId) + .with(user(principal)) // usuario normal + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isForbidden()); } @Test - void cancelReservation_pastDates_shouldReturnConflict() throws Exception { - Long reservationId = 1L; + void deleteReservation_notFound_shouldReturnNotFound() throws Exception { + Long reservationId = 999L; - mockMvc.perform(patch(ACCOMMODATIONS_PATH + "/{id}/reservations/cancel", reservationId) - .with(user(principal)) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isConflict()) - .andExpect(jsonPath("$.message").value("Cannot modify a reservation that has already started")); + mockMvc.perform(delete(RESERVATIONS_PATH + "/admin/{id}", reservationId) + .with(user("admin").roles("ADMIN")) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value("Reservation with id '" + reservationId + "' not found")); } + } } diff --git a/src/test/java/com/SleepUp/SU/reservation/ReservationMapperTest.java b/src/test/java/com/SleepUp/SU/reservation/ReservationMapperTest.java index 307439b2..ea3c2781 100644 --- a/src/test/java/com/SleepUp/SU/reservation/ReservationMapperTest.java +++ b/src/test/java/com/SleepUp/SU/reservation/ReservationMapperTest.java @@ -56,8 +56,7 @@ public void testToSummaryMapping() { assertEquals(reservation.getCheckInDate(), summary.checkInDate()); assertEquals(reservation.getCheckOutDate(), summary.checkOutDate()); assertEquals(reservation.getBookingStatus(), summary.bookingStatus()); - assertEquals(reservation.getEmailSent(), summary.emailSent()); - assertEquals(reservation.getCreatedDate(), summary.createdDate()); + assertEquals(reservation.getTotalPrice(), summary.totalPrice()); } @Test diff --git a/src/test/java/com/SleepUp/SU/reservation/ReservationServiceImplTest.java b/src/test/java/com/SleepUp/SU/reservation/ReservationServiceImplTest.java index 351ad7dd..01eb05e3 100644 --- a/src/test/java/com/SleepUp/SU/reservation/ReservationServiceImplTest.java +++ b/src/test/java/com/SleepUp/SU/reservation/ReservationServiceImplTest.java @@ -7,7 +7,7 @@ import com.SleepUp.SU.reservation.entity.Reservation; import com.SleepUp.SU.reservation.exceptions.ReservationAccommodationOwnerException; import com.SleepUp.SU.reservation.repository.ReservationRepository; -import com.SleepUp.SU.reservation.reservationtime.ReservationTime; +import com.SleepUp.SU.reservation.reservationTime.ReservationTime; import com.SleepUp.SU.reservation.service.ReservationServiceImpl; import com.SleepUp.SU.reservation.status.BookingStatus; import com.SleepUp.SU.reservation.utils.ReservationServiceHelper; @@ -24,6 +24,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.context.ActiveProfiles; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -62,7 +63,7 @@ public class ReservationServiceImplTest { @BeforeEach void setUp(){ - mappedDtos = new ReservationResponseSummary(1L, "Maria",1,"María House",null,null,null,null,null); + mappedDtos = new ReservationResponseSummary(1L, "Maria",1,"María House",null,null,null,null); userId = 1L; mockReservations = List.of(new Reservation()); } @@ -139,7 +140,8 @@ void createReservation_validRequest_shouldReturnDetailResponse() { ReservationResponseDetail expectedResponse = new ReservationResponseDetail( 1L, "Test User", 2, "Test Hotel", reservationRequest.checkInDate(), reservationRequest.checkOutDate(), - BookingStatus.PENDING, false, LocalDateTime.now() + BookingStatus.PENDING, false, LocalDateTime.now(), + BigDecimal.valueOf(100) ); when(accommodationServiceHelper.getAccommodationEntityById(accommodationId)).thenReturn(accommodation); @@ -150,7 +152,7 @@ void createReservation_validRequest_shouldReturnDetailResponse() { doNothing().when(reservationServiceHelper).validateAccommodationAvailability(accommodation, reservationRequest); doNothing().when(reservationServiceHelper).validateUserReservationOverlap(user.getId(), reservationRequest); doNothing().when(reservationServiceHelper).validateAccommodationReservationOverlap(accommodationId, reservationRequest); - doNothing().when(emailServiceHelper).sendOwnerReservedNotification(user, accommodation, savedReservation); +// doNothing().when(emailServiceHelper).sendOwnerReservedNotification(user, accommodation, savedReservation, (BigDecimal) 20); ReservationResponseDetail result = reservationServiceImpl.createReservation(reservationRequest, user, accommodationId); @@ -160,7 +162,7 @@ void createReservation_validRequest_shouldReturnDetailResponse() { assertEquals(BookingStatus.PENDING, result.bookingStatus()); verify(reservationRepository).save(mappedReservation); - verify(emailServiceHelper).sendOwnerReservedNotification(user, accommodation, savedReservation); +// verify(emailServiceHelper).sendOwnerReservedNotification(user, accommodation, savedReservation, 20); } @Test @@ -263,60 +265,6 @@ void createReservation_userIsOwner_shouldThrowException() { } } - @Nested - class CancelReservation { - - @Test - void cancelReservation_validReservation_shouldReturnCancelledDetail() { - Long reservationId = 1L; - Reservation testReservation = createTestReservation(); - testReservation.setId(reservationId); - - Accommodation accommodation = new Accommodation(); - accommodation.setName("Test Accommodation"); - testReservation.setAccommodation(accommodation); - - when(reservationServiceHelper.getReservationEntityById(reservationId)).thenReturn(testReservation); - when(reservationRepository.save(testReservation)).thenReturn(testReservation); - - ApiMessage result = reservationServiceImpl.cancelReservation(reservationId); - - assertNotNull(result); - assertTrue(result.getMessage().contains("has been cancelled")); - assertTrue(result.getMessage().contains("Test Accommodation")); - } - - @Test - void cancelReservation_reservationNotFound_shouldThrowException() { - Long reservationId = 1L; - - when(reservationServiceHelper.getReservationEntityById(reservationId)).thenThrow(new RuntimeException("Reservation not found")); - - RuntimeException exception = assertThrows(RuntimeException.class, - () -> reservationServiceImpl.cancelReservation(reservationId)); - - assertEquals("Reservation not found", exception.getMessage()); - } - - @Test - void cancelReservation_alreadyCancelled_shouldThrowException() { - Long reservationId = 1L; - - Reservation testReservation = createTestReservation(); - testReservation.setId(reservationId); - testReservation.setBookingStatus(BookingStatus.CANCELLED); - - when(reservationServiceHelper.getReservationEntityById(reservationId)).thenReturn(testReservation); - doThrow(new IllegalStateException("Cannot modify a cancelled reservation")) - .when(reservationServiceHelper).validateReservationCancellable(testReservation); - - IllegalStateException exception = assertThrows(IllegalStateException.class, - () -> reservationServiceImpl.cancelReservation(reservationId)); - - assertEquals("Cannot modify a cancelled reservation", exception.getMessage()); - } - } - private User createTestUser() { User user = new User(); user.setId(1L); @@ -341,6 +289,7 @@ private Accommodation createTestAccommodation() { accommodation.setGuestNumber(4); accommodation.setAvailableFrom(LocalDate.now()); accommodation.setAvailableTo(LocalDate.now().plusDays(30)); + accommodation.setPrice(10.0); accommodation.setManagedBy(owner); return accommodation; } @@ -355,11 +304,5 @@ private Reservation createTestReservation() { return reservation; } - private ReservationResponseDetail expectedCancelledResponse() { - return new ReservationResponseDetail( - 1L, "Test User", 4, "Test Hotel", - LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), - BookingStatus.CANCELLED, false, LocalDateTime.now() - ); - } + } diff --git a/src/test/java/com/SleepUp/SU/reservation/ReservationTest.java b/src/test/java/com/SleepUp/SU/reservation/ReservationTest.java index 042ce8ed..57071855 100644 --- a/src/test/java/com/SleepUp/SU/reservation/ReservationTest.java +++ b/src/test/java/com/SleepUp/SU/reservation/ReservationTest.java @@ -4,7 +4,7 @@ import com.SleepUp.SU.reservation.dto.ReservationResponseSummary; import com.SleepUp.SU.reservation.entity.Reservation; import com.SleepUp.SU.reservation.repository.ReservationRepository; -import com.SleepUp.SU.reservation.reservationtime.ReservationTime; +import com.SleepUp.SU.reservation.reservationTime.ReservationTime; import com.SleepUp.SU.reservation.service.ReservationServiceImpl; import com.SleepUp.SU.user.entity.CustomUserDetails; import com.SleepUp.SU.user.entity.User; diff --git a/src/test/java/com/SleepUp/SU/reservation/accommodationOwner/ReservationOwnerControllerTest.java b/src/test/java/com/SleepUp/SU/reservation/accommodationOwner/ReservationOwnerControllerTest.java new file mode 100644 index 00000000..70f9f875 --- /dev/null +++ b/src/test/java/com/SleepUp/SU/reservation/accommodationOwner/ReservationOwnerControllerTest.java @@ -0,0 +1,108 @@ +package com.SleepUp.SU.reservation.accommodationOwner; + + +import com.SleepUp.SU.reservation.dto.ReservationAuthRequest; +import com.SleepUp.SU.reservation.dto.ReservationResponseDetail; +import com.SleepUp.SU.reservation.dto.ReservationResponseSummary; +import com.SleepUp.SU.reservation.reservationGuest.ReservationGuestServiceImpl; +import com.SleepUp.SU.reservation.status.BookingStatus; +import com.SleepUp.SU.user.entity.CustomUserDetails; +import com.SleepUp.SU.user.entity.User; +import com.SleepUp.SU.user.repository.UserRepository; +import com.SleepUp.SU.user.role.Role; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class ReservationOwnerControllerTest { + + private static final String RESERVATIONS_ACCOMMODATION_PATH = "/reservations/accommodation/{id}"; + private static final String RESERVATION_STATUS_PATH = "/reservations/{id}/status"; + private static final String RESERVATION_BY_ID_PATH = "/reservations/{id}"; + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private ReservationOwnerService reservationOwnerServiceImpl; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private UserRepository userRepository; + + private CustomUserDetails principal; + private Long accommodationId = 123L; + + @BeforeEach + void setUp() { + User testUser = userRepository.findByUsername("TestUser").orElseGet(() -> { + User u = new User(); + u.setUsername("TestUser"); + u.setEmail("testuser@example.com"); + u.setName("Test User"); + u.setRole(Role.USER); + return userRepository.save(u); + }); + principal = new CustomUserDetails(testUser); + } + + +// @Nested +// class UpdateReservationStatusTest { +// @Test +// void updateReservationStatus_authorized_shouldReturnOk() throws Exception { +// Long id = 42L; +// ReservationAuthRequest authRequest = new ReservationAuthRequest(BookingStatus.CANCELLED); +// +// ReservationResponseDetail detailDto = new ReservationResponseDetail( +// 42L, +// "alice", +// 2, +// "Beach House", +// LocalDate.of(2025, 9, 25), +// LocalDate.of(2025, 9, 30), +// BookingStatus.CANCELLED, +// true, +// LocalDateTime.of(2025, 9, 1, 10, 30) +// ); +// +// when(reservationOwnerServiceImpl.updateStatus(id, authRequest)) +// .thenReturn(detailDto); +// +// mockMvc.perform(patch(RESERVATION_STATUS_PATH, id) +// .with(user(principal)) +// .contentType(MediaType.APPLICATION_JSON) +// .content(objectMapper.writeValueAsString(authRequest))) +// .andExpect(status().isOk()) +// .andExpect(content().json(objectMapper.writeValueAsString(detailDto))); +// } +// +// } + +} + diff --git a/src/test/java/com/SleepUp/SU/reservation/owner/ReservationOwnerServiceImplTest.java b/src/test/java/com/SleepUp/SU/reservation/accommodationOwner/ReservationOwnerServiceImplTest.java similarity index 56% rename from src/test/java/com/SleepUp/SU/reservation/owner/ReservationOwnerServiceImplTest.java rename to src/test/java/com/SleepUp/SU/reservation/accommodationOwner/ReservationOwnerServiceImplTest.java index f9bcf808..d2e8c39f 100644 --- a/src/test/java/com/SleepUp/SU/reservation/owner/ReservationOwnerServiceImplTest.java +++ b/src/test/java/com/SleepUp/SU/reservation/accommodationOwner/ReservationOwnerServiceImplTest.java @@ -1,17 +1,18 @@ -package com.SleepUp.SU.reservation.owner; +package com.SleepUp.SU.reservation.accommodationOwner; import com.SleepUp.SU.accommodation.entity.Accommodation; -import com.SleepUp.SU.reservation.entity.Reservation; -import com.SleepUp.SU.reservation.repository.ReservationRepository; import com.SleepUp.SU.reservation.dto.ReservationAuthRequest; +import com.SleepUp.SU.reservation.dto.ReservationMapper; import com.SleepUp.SU.reservation.dto.ReservationResponseDetail; import com.SleepUp.SU.reservation.dto.ReservationResponseSummary; -import com.SleepUp.SU.reservation.dto.ReservationMapper; +import com.SleepUp.SU.reservation.entity.Reservation; import com.SleepUp.SU.reservation.exceptions.ReservationNotFoundByIdException; +import com.SleepUp.SU.reservation.repository.ReservationRepository; +import com.SleepUp.SU.reservation.reservationGuest.ReservationGuestServiceImpl; import com.SleepUp.SU.reservation.status.BookingStatus; import com.SleepUp.SU.reservation.utils.ReservationServiceHelper; -import com.SleepUp.SU.utils.EntityUtil; import com.SleepUp.SU.user.entity.User; +import com.SleepUp.SU.utils.EntityUtil; import com.SleepUp.SU.utils.email.EmailServiceHelper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -19,17 +20,15 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.context.ActiveProfiles; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.Collections; -import java.util.List; -import java.util.function.Function; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.*; @ActiveProfiles("test") @@ -71,8 +70,7 @@ void setUp() { LocalDate.of(2025, 9, 25), LocalDate.of(2025, 9, 30), BookingStatus.CONFIRMED, - true, - LocalDateTime.of(2025, 9, 1, 10, 30) + BigDecimal.valueOf(100) ); response2 = new ReservationResponseSummary( 2L, @@ -82,50 +80,49 @@ void setUp() { LocalDate.of(2025, 10, 5), LocalDate.of(2025, 10, 12), BookingStatus.PENDING, - false, - LocalDateTime.of(2025, 9, 15, 14, 0) + BigDecimal.valueOf(100) ); } - @Nested - class getAllReservationsOnMyAccommodationTest{ - @Test - void getAllReservationsOnMyAccommodation_emptyList_shouldThrow() { - when(reservationRepository.findByAccommodationId(accommodationId)) - .thenReturn(Collections.emptyList()); - - assertThatThrownBy(() -> - reservationOwnerServiceImpl.getAllReservationsOnMyAccommodation(dummyUser, accommodationId)) - .isInstanceOf(RuntimeException.class) - .hasMessage("Empty list"); - - verify(reservationRepository).findByAccommodationId(accommodationId); - verify(entityUtil, never()).mapEntitiesToDTOs(anyList(), any()); - } - - @Test - void getAllReservationsOnMyAccommodation_nonEmptyList_shouldReturnDTOs() { - Reservation reservation1 = new Reservation(); - Reservation reservation2 = new Reservation(); - List entities = List.of(reservation1, reservation2); - - List responseList = List.of(response1, response2); - - when(reservationRepository.findByAccommodationId(accommodationId)) - .thenReturn(entities); - - when(entityUtil.mapEntitiesToDTOs( - anyList(), - any(Function.class) - )).thenReturn(responseList); - - List result = - reservationOwnerServiceImpl.getAllReservationsOnMyAccommodation(dummyUser, accommodationId); - - assertThat(result).isSameAs(responseList); - } - } +// @Nested +// class getAllReservationsOnMyAccommodationTest{ +// @Test +// void getAllReservationsOnMyAccommodation_emptyList_shouldThrow() { +// when(reservationRepository.findByAccommodationId(accommodationId)) +// .thenReturn(Collections.emptyList()); +// +// assertThatThrownBy(() -> +// reservationOwnerServiceImpl.getAllReservationsOnMyAccommodation(dummyUser, accommodationId)) +// .isInstanceOf(RuntimeException.class) +// .hasMessage("Empty list"); +// +// verify(reservationRepository).findByAccommodationId(accommodationId); +// verify(entityUtil, never()).mapEntitiesToDTOs(anyList(), any()); +// } +// +// @Test +// void getAllReservationsOnMyAccommodation_nonEmptyList_shouldReturnDTOs() { +// Reservation reservation1 = new Reservation(); +// Reservation reservation2 = new Reservation(); +// List entities = List.of(reservation1, reservation2); +// +// List responseList = List.of(response1, response2); +// +// when(reservationRepository.findByAccommodationId(accommodationId)) +// .thenReturn(entities); +// +// when(entityUtil.mapEntitiesToDTOs( +// anyList(), +// any(Function.class) +// )).thenReturn(responseList); +// +// List result = +// reservationOwnerServiceImpl.getAllReservationsOnMyAccommodation(dummyUser, accommodationId); +// +// assertThat(result).isSameAs(responseList); +// } +// } @Nested class updateStatusTest{ @@ -149,8 +146,20 @@ void updateStatus_existingReservation_shouldUpdateBookingStatusAndReturnDetail() Long id = 77L; ReservationAuthRequest authRequest = new ReservationAuthRequest(BookingStatus.CANCELLED); + Accommodation accommodation = new Accommodation(); + accommodation.setPrice(10.0); + + User user = new User(); + user.setId(1L); + accommodation.setManagedBy(user); + Reservation existing = new Reservation(); existing.setBookingStatus(BookingStatus.PENDING); + existing.setCheckInDate(LocalDate.now()); + existing.setCheckOutDate(LocalDate.now().plusDays(3)); + existing.setAccommodation(accommodation); + + when(reservationServiceHelper.getReservationEntityById(id)).thenReturn(existing); ReservationResponseDetail detailDto = mock(ReservationResponseDetail.class); @@ -166,39 +175,5 @@ void updateStatus_existingReservation_shouldUpdateBookingStatusAndReturnDetail() } } - @Nested - class getReservationByIdTest { - - @Test - void getReservationById_nonExisting_shouldThrow() { - Long id = 123L; - - when(reservationServiceHelper.getReservationEntityById(id)).thenThrow(new ReservationNotFoundByIdException(id)); - - assertThatThrownBy(() -> - reservationOwnerServiceImpl.getReservationById(id)) - .isInstanceOf(ReservationNotFoundByIdException.class) - .hasMessage("Reservation with id '123' not found"); - - verify(reservationMapper, never()).toDetail(any()); - } - - @Test - void getReservationById_existing_shouldReturnDetail() { - Long id = 456L; - Reservation isExisting = new Reservation(); - - when(reservationServiceHelper.getReservationEntityById(id)).thenReturn(isExisting); - ReservationResponseDetail detailDto = mock(ReservationResponseDetail.class); - when(reservationMapper.toDetail(isExisting)) - .thenReturn(detailDto); - - ReservationResponseDetail result = - reservationOwnerServiceImpl.getReservationById(id); - - assertThat(result).isSameAs(detailDto); - verify(reservationMapper).toDetail(isExisting); - } - } } diff --git a/src/test/java/com/SleepUp/SU/reservation/admin/ReservationAdminControllerTest.java b/src/test/java/com/SleepUp/SU/reservation/admin/ReservationAdminControllerTest.java new file mode 100644 index 00000000..0d8e56ec --- /dev/null +++ b/src/test/java/com/SleepUp/SU/reservation/admin/ReservationAdminControllerTest.java @@ -0,0 +1,122 @@ +package com.SleepUp.SU.reservation.admin; + +import com.SleepUp.SU.accommodation.repository.AccommodationRepository; +import com.SleepUp.SU.reservation.controller.ReservationController; +import com.SleepUp.SU.reservation.repository.ReservationRepository; +import com.SleepUp.SU.reservation.service.ReservationServiceImpl; +import com.SleepUp.SU.user.admin.UserAdminServiceImpl; +import com.SleepUp.SU.user.entity.CustomUserDetails; +import com.SleepUp.SU.user.entity.User; +import com.SleepUp.SU.user.repository.UserRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +@Transactional +public class ReservationAdminControllerTest { + + private static final String RESERVATIONS_PATH = "/reservations"; + private static final String ACCOMMODATIONS_PATH = "/accommodations"; + + @Autowired + private WebApplicationContext context; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private UserAdminServiceImpl userAdminServiceImpl; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ReservationRepository reservationRepository; + + @Autowired + private ReservationServiceImpl reservationServiceImpl; + + @Autowired + private ReservationController reservationController; + + @Autowired + private AccommodationRepository accommodationRepository; + + private CustomUserDetails principal; + + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(SecurityMockMvcConfigurers.springSecurity()) + .build(); + + User savedUser = userRepository.findByUsername("User2") + .orElseThrow(() -> new RuntimeException("User2 not found")); + principal = new CustomUserDetails(savedUser); + } + + + @Nested + class DeleteReservationAdminTests { + + @Test + void deleteReservation_asAdmin_shouldReturnNoContent() throws Exception { + Long reservationId = 3L; + + mockMvc.perform(delete(RESERVATIONS_PATH + "/admin/{id}", reservationId) + .with(user("admin").roles("ADMIN")) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isNoContent()); + } + + @Test + void deleteReservation_nonAdmin_shouldReturnForbidden() throws Exception { + Long reservationId = 3L; + + mockMvc.perform(delete(RESERVATIONS_PATH + "/admin/{id}", reservationId) + .with(user(principal)) // usuario normal + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isForbidden()); + } + + @Test + void deleteReservation_notFound_shouldReturnNotFound() throws Exception { + Long reservationId = 999L; + + mockMvc.perform(delete(RESERVATIONS_PATH + "/admin/{id}", reservationId) + .with(user("admin").roles("ADMIN")) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value("Reservation with id '" + reservationId + "' not found")); + } + + } +} diff --git a/src/test/java/com/SleepUp/SU/reservation/admin/ReservationAdminServiceImplTest.java b/src/test/java/com/SleepUp/SU/reservation/admin/ReservationAdminServiceImplTest.java index 7497966c..c1ab1dfb 100644 --- a/src/test/java/com/SleepUp/SU/reservation/admin/ReservationAdminServiceImplTest.java +++ b/src/test/java/com/SleepUp/SU/reservation/admin/ReservationAdminServiceImplTest.java @@ -1,18 +1,14 @@ package com.SleepUp.SU.reservation.admin; -import com.SleepUp.SU.accommodation.dto.AccommodationResponseSummary; import com.SleepUp.SU.reservation.dto.ReservationMapper; -import com.SleepUp.SU.reservation.dto.ReservationResponseDetail; import com.SleepUp.SU.reservation.dto.ReservationResponseSummary; import com.SleepUp.SU.reservation.entity.Reservation; -import com.SleepUp.SU.reservation.owner.ReservationOwnerServiceImpl; +import com.SleepUp.SU.reservation.exceptions.ReservationNotFoundByIdException; import com.SleepUp.SU.reservation.repository.ReservationRepository; import com.SleepUp.SU.reservation.status.BookingStatus; import com.SleepUp.SU.reservation.utils.ReservationServiceHelper; import com.SleepUp.SU.user.entity.User; import com.SleepUp.SU.utils.EntityUtil; -import net.bytebuddy.asm.Advice; -import org.assertj.core.api.AssertionsForInterfaceTypes; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -22,15 +18,16 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.context.ActiveProfiles; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.Collections; import java.util.List; +import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.*; @ActiveProfiles("test") @@ -70,8 +67,7 @@ void setUp() { LocalDate.of(2025, 9, 25), LocalDate.of(2025, 9, 30), BookingStatus.CONFIRMED, - true, - LocalDateTime.of(2025, 9, 1, 10, 30) + BigDecimal.valueOf(100) ); response2 = new ReservationResponseSummary( 2L, @@ -81,11 +77,20 @@ void setUp() { LocalDate.of(2025, 10, 5), LocalDate.of(2025, 10, 12), BookingStatus.PENDING, - false, - LocalDateTime.of(2025, 9, 15, 14, 0) + BigDecimal.valueOf(100) ); } + private Reservation createTestReservation() { + Reservation reservation = new Reservation(); + reservation.setCheckInDate(LocalDate.now().plusDays(1)); + reservation.setCheckOutDate(LocalDate.now().plusDays(3)); + reservation.setGuestNumber(2); + reservation.setBookingStatus(BookingStatus.PENDING); + reservation.setEmailSent(false); + return reservation; + } + @Nested class getAllReservationsToAdminReservationTest{ @Test @@ -104,4 +109,35 @@ void getAllReservationsToAdmin_shouldReturnAllReservations() { verify(reservationMapper, times(2)).toSummary(any(Reservation.class)); } } + + @Nested + class DeleteReservationByAdmin { + + @Test + void deleteReservationByAdmin_validReservation_shouldDeleteSuccessfully() { + Long reservationId = 1L; + Reservation testReservation = createTestReservation(); + testReservation.setId(reservationId); + + when(reservationRepository.existsById(reservationId)).thenReturn(true); + doNothing().when(reservationRepository).deleteById(reservationId); + + assertDoesNotThrow(() -> reservationAdminServiceImpl.deleteReservationByAdmin(reservationId)); + verify(reservationRepository).deleteById(reservationId); + verify(reservationRepository).existsById(reservationId); + } + + @Test + void deleteReservationByAdmin_reservationNotFound_shouldThrowException() { + Long reservationId = 999L; + + when(reservationRepository.existsById(reservationId)).thenReturn(false); + + ReservationNotFoundByIdException exception = assertThrows(ReservationNotFoundByIdException.class, + () -> reservationAdminServiceImpl.deleteReservationByAdmin(reservationId)); + + assertEquals("Reservation with id '" +reservationId + "' not found", exception.getMessage()); + verify(reservationRepository).existsById(reservationId); + } + } } \ No newline at end of file diff --git a/src/test/java/com/SleepUp/SU/reservation/owner/ReservationOwnerControllerTest.java b/src/test/java/com/SleepUp/SU/reservation/owner/ReservationOwnerControllerTest.java deleted file mode 100644 index e82b8b99..00000000 --- a/src/test/java/com/SleepUp/SU/reservation/owner/ReservationOwnerControllerTest.java +++ /dev/null @@ -1,184 +0,0 @@ -package com.SleepUp.SU.reservation.owner; - -import com.SleepUp.SU.reservation.dto.ReservationAuthRequest; -import com.SleepUp.SU.reservation.dto.ReservationResponseDetail; -import com.SleepUp.SU.reservation.dto.ReservationResponseSummary; -import com.SleepUp.SU.reservation.status.BookingStatus; -import com.SleepUp.SU.user.entity.CustomUserDetails; -import com.SleepUp.SU.user.entity.User; -import com.SleepUp.SU.user.repository.UserRepository; -import com.SleepUp.SU.user.role.Role; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.web.servlet.MockMvc; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; - -import static org.mockito.Mockito.when; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@SpringBootTest -@AutoConfigureMockMvc -@ActiveProfiles("test") -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -public class ReservationOwnerControllerTest { - - private static final String RESERVATIONS_ACCOMMODATION_PATH = "/reservations/accommodation/{id}"; - private static final String RESERVATION_STATUS_PATH = "/reservations/{id}/status"; - private static final String RESERVATION_BY_ID_PATH = "/reservations/{id}"; - - @Autowired - private MockMvc mockMvc; - - @MockitoBean - private ReservationOwnerServiceImpl reservationOwnerServiceImpl; - - @Autowired - private ObjectMapper objectMapper; - - @Autowired - private UserRepository userRepository; - - private CustomUserDetails principal; - private Long accommodationId = 123L; - - @BeforeEach - void setUp() { - User testUser = userRepository.findByUsername("TestUser").orElseGet(() -> { - User u = new User(); - u.setUsername("TestUser"); - u.setEmail("testuser@example.com"); - u.setName("Test User"); - u.setRole(Role.USER); - return userRepository.save(u); - }); - principal = new CustomUserDetails(testUser); - } - - @Nested - class GetReservationOnMyAccommodationTest { - @Test - void getReservationOnMyAccommodation_authorized_shouldReturnOk() throws Exception { - List summaries = List.of( - new ReservationResponseSummary( - 1L, - "alice", - 2, - "Beach House", - LocalDate.of(2025, 9, 25), - LocalDate.of(2025, 9, 30), - BookingStatus.CONFIRMED, - true, - LocalDateTime.of(2025, 9, 1, 10, 30) - ), - new ReservationResponseSummary( - 2L, - "bob", - 4, - "Mountain Cabin", - LocalDate.of(2025, 10, 5), - LocalDate.of(2025, 10, 12), - BookingStatus.PENDING, - false, - LocalDateTime.of(2025, 9, 15, 14, 0) - ) - ); - - when(reservationOwnerServiceImpl - .getAllReservationsOnMyAccommodation(principal.getUser(), accommodationId)) - .thenReturn(summaries); - - mockMvc.perform(get(RESERVATIONS_ACCOMMODATION_PATH, accommodationId) - .with(user(principal)) - .param("id", accommodationId.toString()) - .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(content() - .json(objectMapper.writeValueAsString(summaries))); - } - - @Test - void getReservationOnMyAccommodation_withoutAuthentication_shouldReturnUnauthorized() throws Exception { - mockMvc.perform(get(RESERVATIONS_ACCOMMODATION_PATH, accommodationId) - .with(anonymous())) - .andExpect(status().isUnauthorized()); - } - } - - @Nested - class UpdateReservationStatusTest { - @Test - void updateReservationStatus_authorized_shouldReturnOk() throws Exception { - Long id = 42L; - ReservationAuthRequest authRequest = new ReservationAuthRequest(BookingStatus.CANCELLED); - - ReservationResponseDetail detailDto = new ReservationResponseDetail( - 42L, - "alice", - 2, - "Beach House", - LocalDate.of(2025, 9, 25), - LocalDate.of(2025, 9, 30), - BookingStatus.CANCELLED, - true, - LocalDateTime.of(2025, 9, 1, 10, 30) - ); - - when(reservationOwnerServiceImpl.updateStatus(id, authRequest)) - .thenReturn(detailDto); - - mockMvc.perform(patch(RESERVATION_STATUS_PATH, id) - .with(user(principal)) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(authRequest))) - .andExpect(status().isOk()) - .andExpect(content().json(objectMapper.writeValueAsString(detailDto))); - } - - } - - @Nested - class GetReservationByIdTest { - - @Test - void getReservationById_authorized_shouldReturnOk() throws Exception { - Long id = 101L; - - ReservationResponseDetail detailDto = new ReservationResponseDetail( - 101L, - "charlie", - 3, - "Lake Cabin", - LocalDate.of(2025, 11, 1), - LocalDate.of(2025, 11, 5), - BookingStatus.CONFIRMED, - true, - LocalDateTime.of(2025, 10, 20, 9, 15) - ); - - when(reservationOwnerServiceImpl.getReservationById(id)) - .thenReturn(detailDto); - - mockMvc.perform(post(RESERVATION_BY_ID_PATH, id) - .with(user(principal)) - .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(content().json(objectMapper.writeValueAsString(detailDto))); - } - - } -} diff --git a/src/test/java/com/SleepUp/SU/reservation/reservationGuest/ReservationGuestControllerTest.java b/src/test/java/com/SleepUp/SU/reservation/reservationGuest/ReservationGuestControllerTest.java new file mode 100644 index 00000000..bb192a69 --- /dev/null +++ b/src/test/java/com/SleepUp/SU/reservation/reservationGuest/ReservationGuestControllerTest.java @@ -0,0 +1,163 @@ +package com.SleepUp.SU.reservation.reservationGuest; + +import com.SleepUp.SU.reservation.dto.ReservationRequest; +import com.SleepUp.SU.reservation.repository.ReservationRepository; +import com.SleepUp.SU.user.entity.CustomUserDetails; +import com.SleepUp.SU.user.entity.User; +import com.SleepUp.SU.user.repository.UserRepository; +import com.SleepUp.SU.user.role.Role; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +import static org.hamcrest.Matchers.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +@Transactional +public class ReservationGuestControllerTest { + + private static final String RESERVATION_CANCEL_PATH = "/reservations/{id}/cancel"; + private static final String RESERVATION_UPDATE_PATH = "/reservations/{id}"; + private static final String RESERVATION_BY_ID_PATH = "/reservations/{id}"; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ReservationRepository reservationRepository; + + private CustomUserDetails principal; + private CustomUserDetails principal2; + private Long reservationId; + + @BeforeEach + void setUp() { + User testUser = userRepository.findByUsername("User1") + .orElseThrow(() -> new RuntimeException("User1 not found")); + + principal = new CustomUserDetails(testUser); + + User testUser2 = userRepository.findByUsername("User4") + .orElseThrow(() -> new RuntimeException("User4 not found")); + + principal2 = new CustomUserDetails(testUser2); + + reservationId = reservationRepository.findByUser_Id(testUser.getId()).getFirst().getId(); + } + + @Nested + class GetReservationByIdTest { + + @Test + void getReservationById_authorized_shouldReturnOk() throws Exception { + + mockMvc.perform(get(RESERVATION_BY_ID_PATH, reservationId) + .with(user(principal)) + .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(5)) + .andExpect(jsonPath("$.userName").value("Name1")) + .andExpect(jsonPath("$.guestNumber").isEmpty()) + .andExpect(jsonPath("$.accommodationName").value("Mountain View Cabin")) + .andExpect(jsonPath("$.checkInDate").value("2025-11-02")) + .andExpect(jsonPath("$.checkOutDate").value("2025-11-08")) + .andExpect(jsonPath("$.bookingStatus").value("PENDING")) + .andExpect(jsonPath("$.emailSent").value(false)); + } + + } + + @Nested + class UpdateReservationByIdTest { + + + @Test + void updateReservation_validRequest_shouldReturnUpdatedMessage() throws Exception { + + ReservationRequest updateRequest = ReservationRequest.builder() + .guestNumber(3) + .checkInDate(LocalDate.of(2025, 11, 2)) + .checkOutDate(LocalDate.of(2025, 11, 8)) + .build(); + + mockMvc.perform(put(RESERVATION_UPDATE_PATH, reservationId) + .with(user(principal)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updateRequest))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.message", allOf( + containsString("updated"), + containsString("reservation") + ))); + } + } + + @Nested + class CancelReservationTests { + + @Test + void cancelReservation_validRequest_shouldReturnCancelledReservation2() throws Exception { + + mockMvc.perform(patch(RESERVATION_CANCEL_PATH, reservationId) + .with(user(principal))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.message", allOf(containsString("reservation"), containsString("cancelled")))); + } + + + @Test + void cancelReservation_alreadyStarted_shouldReturnConflict() throws Exception { + Long reservationId = 1L; + + mockMvc.perform(patch(RESERVATION_CANCEL_PATH, reservationId) + .with(user(principal2))) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.message").value("Cannot modify a reservation that has already started")); + } + + @Test + void cancelReservation_pastDates_shouldReturnConflict() throws Exception { + Long reservationId = 1L; + + mockMvc.perform(patch(RESERVATION_CANCEL_PATH, reservationId) + .with(user(principal2)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.message").value("Cannot modify a reservation that has already started")); + } + } +} + diff --git a/src/test/java/com/SleepUp/SU/reservation/reservationGuest/ReservationGuestServiceImplTest.java b/src/test/java/com/SleepUp/SU/reservation/reservationGuest/ReservationGuestServiceImplTest.java new file mode 100644 index 00000000..08c6aa9f --- /dev/null +++ b/src/test/java/com/SleepUp/SU/reservation/reservationGuest/ReservationGuestServiceImplTest.java @@ -0,0 +1,192 @@ +package com.SleepUp.SU.reservation.reservationGuest; + +import com.SleepUp.SU.reservation.entity.Reservation; +import com.SleepUp.SU.reservation.repository.ReservationRepository; +import com.SleepUp.SU.reservation.dto.ReservationResponseDetail; +import com.SleepUp.SU.reservation.dto.ReservationResponseSummary; +import com.SleepUp.SU.reservation.dto.ReservationMapper; +import com.SleepUp.SU.reservation.exceptions.ReservationNotFoundByIdException; +import com.SleepUp.SU.reservation.status.BookingStatus; +import com.SleepUp.SU.reservation.utils.ReservationServiceHelper; +import com.SleepUp.SU.utils.EntityUtil; +import com.SleepUp.SU.user.entity.User; +import com.SleepUp.SU.utils.dto.ApiMessageDto; +import com.SleepUp.SU.utils.email.EmailServiceHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +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.test.context.ActiveProfiles; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) +class ReservationGuestServiceImplTest { + + @Mock + private ReservationRepository reservationRepository; + + @Mock + private ReservationMapper reservationMapper; + + @Mock + private ReservationServiceHelper reservationServiceHelper; + + @Mock + private EmailServiceHelper emailServiceHelper; + + @Mock + private EntityUtil entityUtil; + + @InjectMocks + private ReservationGuestServiceImpl reservationGuestServiceImpl; + + private final User dummyUser = new User(); + private final Long accommodationId = 55L; + + private ReservationResponseSummary response1; + private ReservationResponseSummary response2; + + + @BeforeEach + void setUp() { + response1 = new ReservationResponseSummary( + 1L, + "alice", + 2, + "Beach House", + LocalDate.of(2025, 9, 25), + LocalDate.of(2025, 9, 30), + BookingStatus.CONFIRMED, + BigDecimal.valueOf(100) + ); + response2 = new ReservationResponseSummary( + 2L, + "bob", + 4, + "Mountain Cabin", + LocalDate.of(2025, 10, 5), + LocalDate.of(2025, 10, 12), + BookingStatus.PENDING, + BigDecimal.valueOf(100) + ); + + } + + @Nested + class getReservationByIdTest { + + @Test + void getReservationById_nonExisting_shouldThrow() { + Long id = 123L; + + when(reservationServiceHelper.getReservationEntityById(id)).thenThrow(new ReservationNotFoundByIdException(id)); + + assertThatThrownBy(() -> + reservationGuestServiceImpl.getReservationById(id)) + .isInstanceOf(ReservationNotFoundByIdException.class) + .hasMessage("Reservation with id '123' not found"); + + verify(reservationMapper, never()).toDetail(any()); + } + + @Test + void getReservationById_existing_shouldReturnDetail() { + Long id = 456L; + Reservation isExisting = new Reservation(); + + when(reservationServiceHelper.getReservationEntityById(id)).thenReturn(isExisting); + ReservationResponseDetail detailDto = mock(ReservationResponseDetail.class); + when(reservationMapper.toDetail(isExisting)) + .thenReturn(detailDto); + + ReservationResponseDetail result = + reservationGuestServiceImpl.getReservationById(id); + + assertThat(result).isSameAs(detailDto); + verify(reservationMapper).toDetail(isExisting); + } + } + + + @Nested + class CancelReservation { + +// @Test +// void cancelReservation_validReservation_shouldReturnCancelledDetail() { +// Long reservationId = 1L; +// Reservation testReservation = createTestReservation(); +// testReservation.setId(reservationId); +// +// ReservationResponseDetail expectedResponse = expectedCancelledResponse(); +// +// when(reservationServiceHelper.getReservationEntityById(reservationId)).thenReturn(testReservation); +// when(reservationRepository.save(testReservation)).thenReturn(testReservation); +// when(reservationMapper.toDetail(testReservation)).thenReturn(expectedResponse); +// +// ApiMessageDto result = reservationGuestServiceImpl.cancelReservation(reservationId); +// +// assertNotNull(result); +//// assertEquals(BookingStatus.CANCELLED, result.bookingStatus()); +// } + + @Test + void cancelReservation_reservationNotFound_shouldThrowException() { + Long reservationId = 1L; + + when(reservationServiceHelper.getReservationEntityById(reservationId)).thenThrow(new RuntimeException("Reservation not found")); + + RuntimeException exception = assertThrows(RuntimeException.class, + () -> reservationGuestServiceImpl.cancelReservation(reservationId)); + + assertEquals("Reservation not found", exception.getMessage()); + } + + @Test + void cancelReservation_alreadyCancelled_shouldThrowException() { + Long reservationId = 1L; + + Reservation testReservation = createTestReservation(); + testReservation.setId(reservationId); + testReservation.setBookingStatus(BookingStatus.CANCELLED); + + when(reservationServiceHelper.getReservationEntityById(reservationId)).thenReturn(testReservation); + doThrow(new IllegalStateException("Cannot modify a cancelled reservation")) + .when(reservationServiceHelper).validateReservationCancellable(testReservation); + + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> reservationGuestServiceImpl.cancelReservation(reservationId)); + + assertEquals("Cannot modify a cancelled reservation", exception.getMessage()); + } + } + + + private Reservation createTestReservation() { + Reservation reservation = new Reservation(); + reservation.setCheckInDate(LocalDate.now().plusDays(1)); + reservation.setCheckOutDate(LocalDate.now().plusDays(3)); + reservation.setGuestNumber(2); + reservation.setBookingStatus(BookingStatus.PENDING); + reservation.setEmailSent(false); + return reservation; + } + + private ReservationResponseDetail expectedCancelledResponse() { + return new ReservationResponseDetail( + 1L, "Test User", 4, "Test Hotel", + LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), + BookingStatus.CANCELLED, false, LocalDateTime.now(), BigDecimal.valueOf(100) + ); + } +} diff --git a/src/test/java/com/SleepUp/SU/reservation/security/ReservationAccessEvaluatorTest.java b/src/test/java/com/SleepUp/SU/reservation/security/ReservationAccessEvaluatorTest.java index ad103f51..eed6e5d9 100644 --- a/src/test/java/com/SleepUp/SU/reservation/security/ReservationAccessEvaluatorTest.java +++ b/src/test/java/com/SleepUp/SU/reservation/security/ReservationAccessEvaluatorTest.java @@ -1,51 +1,150 @@ package com.SleepUp.SU.reservation.security; -import com.SleepUp.SU.reservation.repository.ReservationRepository; -import org.junit.jupiter.api.BeforeEach; +import com.SleepUp.SU.accommodation.entity.Accommodation; +import com.SleepUp.SU.accommodation.utils.AccommodationServiceHelper; +import com.SleepUp.SU.reservation.entity.Reservation; +import com.SleepUp.SU.reservation.utils.ReservationServiceHelper; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.access.AccessDeniedException; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +@ExtendWith(MockitoExtension.class) class ReservationAccessEvaluatorTest { - private ReservationRepository reservationRepository; + @Mock + private ReservationServiceHelper reservationServiceHelper; + + @Mock + private AccommodationServiceHelper accommodationServiceHelper; + + @InjectMocks private ReservationAccessEvaluator reservationAccessEvaluator; - @BeforeEach - void setUp() { - reservationRepository = mock(ReservationRepository.class); - reservationAccessEvaluator = new ReservationAccessEvaluator(reservationRepository); + private final Reservation reservationMock = mock(Reservation.class); + private final Accommodation accommodationMock = mock(Accommodation.class); + + private final Long reservationId = 1L; + private final Long userId = 2L; + private final Long accommodationId = 100L; + + @Nested + class IsReservationGuest { + + @Test + void isReservationGuest_reservationExistsForUser_shouldReturnTrue() { + + when(reservationServiceHelper.isReservationGuestTheUser(reservationId, userId)).thenReturn(true); + + boolean result = reservationAccessEvaluator.isReservationGuest(reservationId, userId); + assertTrue(result); + + verify(reservationServiceHelper, times(1)).isReservationGuestTheUser(reservationId, userId); + } + + @Test + void isReservationGuest_reservationDoesNotExistForUser_shouldReturnFalse() { + + when(reservationServiceHelper.isReservationGuestTheUser(reservationId, userId)).thenReturn(false); + + AccessDeniedException exception = assertThrows(AccessDeniedException.class, () -> { + reservationAccessEvaluator.isReservationGuest(reservationId, userId); + }); + + String expectedMessage = "Cause: This reservation was not created by you."; + assertEquals(expectedMessage, exception.getMessage()); + + verify(reservationServiceHelper, times(1)).isReservationGuestTheUser(reservationId, userId); + } } - @Test - void testIsReservationGuest_true() { - Long reservationId = 1L; - Long userId = 2L; + @Nested + class IsReservationGuestOrOwner { + + @Test + void isReservationGuestOrOwner_guestIsTrue_shouldReturnTrue() { + + when(reservationServiceHelper.getAccommodationIdFromReservationId(reservationId)).thenReturn(accommodationId); + when(reservationServiceHelper.isReservationGuestTheUser(reservationId, userId)).thenReturn(true); - when(reservationRepository.existsByIdAndUser_Id(reservationId, userId)).thenReturn(true); + boolean result = reservationAccessEvaluator.isReservationGuestOrOwner(reservationId, userId); + assertTrue(result); - boolean result = reservationAccessEvaluator.isReservationGuest(reservationId, userId); - assertTrue(result); + verify(reservationServiceHelper).isReservationGuestTheUser(reservationId, userId); + } - verify(reservationRepository, times(1)).existsByIdAndUser_Id(reservationId, userId); + @Test + void isReservationGuestOrOwner_ownerIsTrue_shouldReturnTrue() { + + when(reservationServiceHelper.getAccommodationIdFromReservationId(reservationId)).thenReturn(accommodationId); + when(reservationServiceHelper.isReservationGuestTheUser(reservationId, userId)).thenReturn(false); + when(accommodationServiceHelper.isAccommodationOwnedByUser(accommodationId, userId)).thenReturn(true); + + boolean result = reservationAccessEvaluator.isReservationGuestOrOwner(reservationId, userId); + assertTrue(result); + + verify(reservationServiceHelper).isReservationGuestTheUser(reservationId, userId); + verify(accommodationServiceHelper).isAccommodationOwnedByUser(accommodationId, userId); + } + + @Test + void isReservationGuestOrOwner_neitherGuestNorOwner_shouldReturnFalse() { + + when(reservationServiceHelper.getAccommodationIdFromReservationId(reservationId)).thenReturn(accommodationId); + when(reservationServiceHelper.isReservationGuestTheUser(reservationId, userId)).thenReturn(false); + when(accommodationServiceHelper.isAccommodationOwnedByUser(accommodationId, userId)).thenReturn(false); + + AccessDeniedException exception = assertThrows(AccessDeniedException.class, () -> { + reservationAccessEvaluator.isReservationGuestOrOwner(reservationId, userId); + }); + + String expectedMessage = "Cause: This reservation was not created by you or does not belong to any of your accommodations."; + assertEquals(expectedMessage, exception.getMessage()); + + + + verify(reservationServiceHelper).isReservationGuestTheUser(reservationId, userId); + verify(accommodationServiceHelper).isAccommodationOwnedByUser(accommodationId, userId); + } } - @Test - void testIsReservationGuest_false() { - Long reservationId = 1L; - Long userId = 2L; + @Nested + class IsReservationAccommodationOwner { + + @Test + void isReservationAccommodationOwner_ownerIsTrue_shouldReturnTrue() { + when(reservationServiceHelper.getAccommodationIdFromReservationId(reservationId)).thenReturn(accommodationId); + when(accommodationServiceHelper.isAccommodationOwnedByUser(accommodationId, userId)).thenReturn(true); + + boolean result = reservationAccessEvaluator.isReservationAccommodationOwner(reservationId, userId); + assertTrue(result); + + verify(accommodationServiceHelper).isAccommodationOwnedByUser(accommodationId, userId); + verify(reservationServiceHelper).getAccommodationIdFromReservationId(reservationId); + } + + @Test + void isReservationAccommodationOwner_ownerIsFalse_shouldReturnFalse() { + + when(reservationServiceHelper.getAccommodationIdFromReservationId(reservationId)).thenReturn(accommodationId); + when(accommodationServiceHelper.isAccommodationOwnedByUser(accommodationId, userId)).thenReturn(false); - when(reservationRepository.existsByIdAndUser_Id(reservationId, userId)).thenReturn(false); + AccessDeniedException exception = assertThrows(AccessDeniedException.class, () -> { + reservationAccessEvaluator.isReservationAccommodationOwner(reservationId, userId); + }); - AccessDeniedException exception = assertThrows(AccessDeniedException.class, () -> { - reservationAccessEvaluator.isReservationGuest(reservationId, userId); - }); + String expectedMessage = "Cause: This reservation does not belong to any of your accommodations."; + assertEquals(expectedMessage, exception.getMessage()); - String expectedMessage = "User ID 2 cannot access Reservation ID 1. Only reservation guests can access this information."; - assertEquals(expectedMessage, exception.getMessage()); - verify(reservationRepository, times(1)).existsByIdAndUser_Id(reservationId, userId); + verify(accommodationServiceHelper).isAccommodationOwnedByUser(accommodationId, userId); + verify(reservationServiceHelper).getAccommodationIdFromReservationId(reservationId); + } } } diff --git a/src/test/java/com/SleepUp/SU/security/CustomAccessDeniedHandlerTest.java b/src/test/java/com/SleepUp/SU/security/CustomAccessDeniedHandlerTest.java new file mode 100644 index 00000000..97d13901 --- /dev/null +++ b/src/test/java/com/SleepUp/SU/security/CustomAccessDeniedHandlerTest.java @@ -0,0 +1,72 @@ +package com.SleepUp.SU.security; + +import com.SleepUp.SU.exceptions.ErrorResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; + +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class CustomAccessDeniedHandlerTest { + + @Mock + private ObjectMapper objectMapper; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private AccessDeniedException accessDeniedException; + + private CustomAccessDeniedHandler accessDeniedHandler; + + private StringWriter responseWriter; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + accessDeniedHandler = new CustomAccessDeniedHandler(objectMapper); + + responseWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(responseWriter); + when(response.getWriter()).thenReturn(printWriter); + + when(accessDeniedException.getMessage()).thenReturn("Forbidden access"); + when(request.getRequestURI()).thenReturn("/test/uri"); + } + + @Test + void handle_givenAccessDeniedException_shouldWriteJsonAndSetStatus() throws Exception { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.FORBIDDEN, + "Access Denied. Forbidden access", + request); + String jsonResponse = "{\"status\":\"FORBIDDEN\",\"message\":\"Access Denied. Forbidden access\",\"path\":\"/test/uri\"}"; + + when(objectMapper.writeValueAsString(any(ErrorResponse.class))).thenReturn(jsonResponse); + + accessDeniedHandler.handle(request, response, accessDeniedException); + + verify(response).setContentType("application/json"); + verify(response).setCharacterEncoding("UTF-8"); + verify(response).setStatus(HttpServletResponse.SC_FORBIDDEN); + + verify(objectMapper).writeValueAsString(any(ErrorResponse.class)); + + String responseOutput = responseWriter.toString(); + assertEquals(jsonResponse, responseOutput); + } +} diff --git a/src/test/java/com/SleepUp/SU/security/health/DatabaseHealthIndicatorTest.java b/src/test/java/com/SleepUp/SU/security/health/DatabaseHealthIndicatorTest.java new file mode 100644 index 00000000..7d81d4cd --- /dev/null +++ b/src/test/java/com/SleepUp/SU/security/health/DatabaseHealthIndicatorTest.java @@ -0,0 +1,59 @@ +package com.SleepUp.SU.security.health; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.health.Health; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +public class DatabaseHealthIndicatorTest { + + private DataSource dataSource; + private Connection connection; + private DatabaseHealthIndicator healthIndicator; + + @BeforeEach + public void setup() { + dataSource = mock(DataSource.class); + connection = mock(Connection.class); + healthIndicator = new DatabaseHealthIndicator(dataSource); + } + + @Test + public void testHealthUp() throws Exception { + when(dataSource.getConnection()).thenReturn(connection); + when(connection.isValid(5)).thenReturn(true); + + Health health = healthIndicator.health(); + + assertEquals(Health.up().withDetail("database", "UP").build().getStatus(), health.getStatus()); + assertEquals("UP", health.getDetails().get("database")); + verify(connection).close(); + } + + @Test + public void testHealthDownInvalid() throws Exception { + when(dataSource.getConnection()).thenReturn(connection); + when(connection.isValid(5)).thenReturn(false); + + Health health = healthIndicator.health(); + + assertEquals(Health.down().withDetail("database", "DOWN").build().getStatus(), health.getStatus()); + assertEquals("DOWN", health.getDetails().get("database")); + verify(connection).close(); + } + + @Test + public void testHealthDownException() throws Exception { + when(dataSource.getConnection()).thenThrow(new SQLException("DB error")); + + Health health = healthIndicator.health(); + + assertEquals(Health.down().build().getStatus(), health.getStatus()); + assertEquals("DOWN", health.getDetails().get("database")); + } +} diff --git a/src/test/java/com/SleepUp/SU/security/health/HealthControllerTest.java b/src/test/java/com/SleepUp/SU/security/health/HealthControllerTest.java new file mode 100644 index 00000000..8093a527 --- /dev/null +++ b/src/test/java/com/SleepUp/SU/security/health/HealthControllerTest.java @@ -0,0 +1,57 @@ +package com.SleepUp.SU.security.health; + +import com.SleepUp.SU.security.RestAuthenticationEntryPoint; +import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthComponent; +import org.springframework.boot.actuate.health.HealthEndpoint; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +@Transactional +class HealthControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private HealthEndpoint healthEndpoint; + + @MockitoBean + private RestAuthenticationEntryPoint restAuthenticationEntryPoint; + + + @Test + public void testHealthUp() throws Exception { + HealthComponent healthComponent = Health.up().build(); + when(healthEndpoint.health()).thenReturn(healthComponent); + + mockMvc.perform(get("/health").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("UP")); + } + + @Test + public void testHealthDown() throws Exception { + HealthComponent healthComponent = Health.down().build(); + when(healthEndpoint.health()).thenReturn(healthComponent); + + mockMvc.perform(get("/health").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isServiceUnavailable()) + .andExpect(jsonPath("$.status").value("DOWN")); + } +} + diff --git a/src/test/java/com/SleepUp/SU/security/jwt/JwtAuthFilterTest.java b/src/test/java/com/SleepUp/SU/security/jwt/JwtAuthFilterTest.java new file mode 100644 index 00000000..6d8f7b88 --- /dev/null +++ b/src/test/java/com/SleepUp/SU/security/jwt/JwtAuthFilterTest.java @@ -0,0 +1,93 @@ +package com.SleepUp.SU.security.jwt; + +import com.SleepUp.SU.auth.TokenBlacklistService; +import com.SleepUp.SU.security.RestAuthenticationEntryPoint; +import com.SleepUp.SU.user.admin.UserAdminServiceImpl; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.mockito.Mockito.*; + +class JwtAuthFilterTest { + + @Mock HttpServletRequest request; + @Mock HttpServletResponse response; + @Mock FilterChain filterChain; + @Mock JwtService jwtService; + @Mock TokenBlacklistService tokenBlacklistService; + @Mock UserAdminServiceImpl userService; + @Mock RestAuthenticationEntryPoint restAuthenticationEntryPoint; + + @InjectMocks + JwtAuthFilter jwtAuthFilter; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + SecurityContextHolder.clearContext(); + } + + @Test + void testDoFilterInternal_noAuthHeader_callsFilterChain() throws Exception { + when(request.getHeader("Authorization")).thenReturn(null); + jwtAuthFilter.doFilterInternal(request, response, filterChain); + verify(filterChain).doFilter(request, response); + verifyNoInteractions(jwtService, userService, restAuthenticationEntryPoint); + } + + @Test + void testDoFilterInternal_invalidAuthHeader_callsFilterChain() throws Exception { + when(request.getHeader("Authorization")).thenReturn("InvalidHeader"); + jwtAuthFilter.doFilterInternal(request, response, filterChain); + verify(filterChain).doFilter(request, response); + verifyNoInteractions(jwtService, userService, restAuthenticationEntryPoint); + } + + @Test + void testDoFilterInternal_invalidToken_throwsException_callsCommence() throws Exception { + String token = "token123"; + when(request.getHeader("Authorization")).thenReturn("Bearer " + token); + when(jwtService.isValidToken(token)).thenReturn(false); + + jwtAuthFilter.doFilterInternal(request, response, filterChain); + + verify(restAuthenticationEntryPoint).commence(eq(request), eq(response), any(AuthenticationCredentialsNotFoundException.class)); + verify(filterChain, never()).doFilter(any(), any()); + } + + @Test + void testDoFilterInternal_tokenInBlacklist_throwsException_callsCommence() throws Exception { + String token = "token123"; + when(request.getHeader("Authorization")).thenReturn("Bearer " + token); + when(jwtService.isValidToken(token)).thenReturn(true); + when(tokenBlacklistService.isTokenInBlacklist(token)).thenReturn(true); + + jwtAuthFilter.doFilterInternal(request, response, filterChain); + + verify(restAuthenticationEntryPoint).commence(eq(request), eq(response), any(AuthenticationCredentialsNotFoundException.class)); + verify(filterChain, never()).doFilter(any(), any()); + } + + @Test + void testDoFilterInternal_authenticationException_callsCommenceAndClearsContext() throws Exception { + String token = "token123"; + when(request.getHeader("Authorization")).thenReturn("Bearer " + token); + when(jwtService.isValidToken(token)).thenReturn(true); + when(tokenBlacklistService.isTokenInBlacklist(token)).thenReturn(false); + when(jwtService.extractUsername(token)).thenReturn("user"); + when(userService.loadUserByUsername("user")).thenThrow(new AuthenticationCredentialsNotFoundException("fail")); + + jwtAuthFilter.doFilterInternal(request, response, filterChain); + + verify(restAuthenticationEntryPoint).commence(eq(request), eq(response), any(AuthenticationException.class)); + assert SecurityContextHolder.getContext().getAuthentication() == null; + verify(filterChain, never()).doFilter(any(), any()); + } +} diff --git a/src/test/java/com/SleepUp/SU/security/jwt/JwtServiceTest.java b/src/test/java/com/SleepUp/SU/security/jwt/JwtServiceTest.java new file mode 100644 index 00000000..f2c7e447 --- /dev/null +++ b/src/test/java/com/SleepUp/SU/security/jwt/JwtServiceTest.java @@ -0,0 +1,70 @@ +package com.SleepUp.SU.security.jwt; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.*; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.userdetails.UserDetails; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class JwtServiceTest { + + private JwtService jwtService; + + @Mock + private UserDetails userDetails; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + + String secret = "YourBase64EncodedSecretHereYourBase64EncodedSecretHere"; + long expirationMs = 60000L; + + jwtService = new JwtService(secret, expirationMs); + } + + @Test + void isValidToken_isTokenExpiredThrows_shouldReturnFalse() { + JwtService spyJwtService = spy(jwtService); + String token = "dummyToken"; + + doThrow(new RuntimeException("expired token")).when(spyJwtService).isTokenExpired(token); + + boolean result = spyJwtService.isValidToken(token); + assertFalse(result); + } + + @Test + void refreshAccessToken_tokenInvalid_shouldThrowRuntimeException() { + String refreshToken = "invalidToken"; + JwtService spyJwtService = spy(jwtService); + + doReturn(false).when(spyJwtService).isValidToken(refreshToken); + + Exception exception = assertThrows(RuntimeException.class, + () -> spyJwtService.refreshAccessToken(refreshToken, userDetails)); + + assertEquals("Invalid or expired refresh token", exception.getMessage()); + } + + @Test + void refreshAccessToken_usernameMismatch_shouldThrowRuntimeException() { + String refreshToken = "validToken"; + + JwtService spyJwtService = spy(jwtService); + when(userDetails.getUsername()).thenReturn("user1"); + + doReturn(true).when(spyJwtService).isValidToken(refreshToken); + doReturn("differentUser").when(spyJwtService).extractUsername(refreshToken); + + Exception exception = assertThrows(RuntimeException.class, + () -> spyJwtService.refreshAccessToken(refreshToken, userDetails)); + + assertEquals("Invalid or expired refresh token", exception.getMessage()); + } +} diff --git a/src/test/java/com/SleepUp/SU/user/UserControllerTest.java b/src/test/java/com/SleepUp/SU/user/UserControllerTest.java index f1dd93e9..56039c4e 100644 --- a/src/test/java/com/SleepUp/SU/user/UserControllerTest.java +++ b/src/test/java/com/SleepUp/SU/user/UserControllerTest.java @@ -24,7 +24,7 @@ public class UserControllerTest { private UserAdminServiceImpl userAdminServiceImpl; @Test - void should_returnAllUsers() { + void listAllUsers_whenUserExist_shouldReturnAllUsers() { List users = List.of( new UserResponse(1L,"", "userOne", "user1@test.com", null), new UserResponse(2L, "","userTwo", "user2@test.com", null) diff --git a/src/test/java/com/SleepUp/SU/user/admin/UserAdminControllerTest.java b/src/test/java/com/SleepUp/SU/user/admin/UserAdminControllerTest.java index a6a29ed1..03d60778 100644 --- a/src/test/java/com/SleepUp/SU/user/admin/UserAdminControllerTest.java +++ b/src/test/java/com/SleepUp/SU/user/admin/UserAdminControllerTest.java @@ -1,10 +1,10 @@ package com.SleepUp.SU.user.admin; +import com.SleepUp.SU.user.dto.UserRequest; import com.SleepUp.SU.user.entity.CustomUserDetails; import com.SleepUp.SU.user.entity.User; import com.SleepUp.SU.user.repository.UserRepository; import com.SleepUp.SU.user.dto.UserRequestAdmin; -import com.SleepUp.SU.user.dto.UserResponse; import com.SleepUp.SU.user.role.Role; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; @@ -14,72 +14,78 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; -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.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") +@Transactional public class UserAdminControllerTest { - @Autowired - private MockMvc mockMvc; + private static final String BASE_PATH = "/users/admin"; + private static final String USER_PATH_ID = "/users/admin/{id}"; - @Autowired - private ObjectMapper objectMapper; + @Autowired + private MockMvc mockMvc; @Autowired - private UserAdminController userAdminController; + private ObjectMapper objectMapper; @Autowired private UserRepository userRepository; - @Autowired - private UserAdminServiceImpl userAdminServiceImpl; - - private CustomUserDetails customUserDetails; - - @BeforeEach - void sepUp(){ - User savedUser = userRepository.findByUsername("Admin1") - .orElseThrow(() -> new RuntimeException("User2 not found")); - - customUserDetails = new CustomUserDetails(savedUser); + @Autowired + private BCryptPasswordEncoder passwordEncoder; + + private CustomUserDetails adminCustomUserDetails; + private CustomUserDetails userCustomUserDetails; + + private UserRequest userRequest; + private UserRequestAdmin updateUserRequestAdmin; + + @BeforeEach + void setUp() { + User savedAdmin = userRepository.findByUsername("Admin1") + .orElseThrow(() -> new RuntimeException("Admin1 not found")); + adminCustomUserDetails = new CustomUserDetails(savedAdmin); + + User savedUser = userRepository.findByUsername("User1") + .orElseThrow(() -> new RuntimeException("User1 not found")); + userCustomUserDetails = new CustomUserDetails(savedUser); + + userRequest = UserRequest.builder() + .username("newUser") + .name("New User") + .email("newuser@test.com") + .password(passwordEncoder.encode("password123")) + .build(); + + updateUserRequestAdmin = UserRequestAdmin.builder() + .username("updatedUser") + .name("Updated Name") + .email("updated@test.com") + .password(passwordEncoder.encode("newPassword")) + .role(Role.ADMIN) + .build(); } - @Nested class CreateUserTest { @Test - void when_adminRole_then_createUser() throws Exception { - UserRequestAdmin request = new UserRequestAdmin( - "newUser", - "New User", - "newuser@test.com", - "password123", - Role.USER - ); - - UserResponse response = new UserResponse( - 10L, - "newUser", - "New User", - "newuser@test.com", - Role.USER - ); - - - mockMvc.perform(post("/api/users/admin") - .with(user(customUserDetails)) + void createUser_whenAdminRole_shouldReturnCreated() throws Exception { + mockMvc.perform(post(BASE_PATH) + .with(user(adminCustomUserDetails)) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) + .content(objectMapper.writeValueAsString(userRequest)) + .param("role", Role.USER.name())) .andExpect(status().isCreated()) .andExpect(jsonPath("$.username").value("newUser")) .andExpect(jsonPath("$.email").value("newuser@test.com")) @@ -87,48 +93,34 @@ void when_adminRole_then_createUser() throws Exception { } @Test - void when_notAdminRole_then_forbidden() throws Exception { - UserRequestAdmin request = new UserRequestAdmin( - "newUser", - "New User", - "newuser@test.com", - "password123", - Role.USER - ); - - mockMvc.perform(post("/api/users/admin") - .with(user("user").roles("USER")) + void createUser_whenNotAdminRole_shouldReturnForbidden() throws Exception { + mockMvc.perform(post(BASE_PATH) + .with(user(userCustomUserDetails)) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) + .content(objectMapper.writeValueAsString(userRequest)) + .param("role", Role.USER.name())) .andExpect(status().isForbidden()); } + + @Test + void createUser_whenNoAuthentication_shouldReturnUnauthorized() throws Exception { + mockMvc.perform(post(BASE_PATH) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userRequest))) + .andExpect(status().isUnauthorized()); + } } @Nested class UpdateUserTest { + @Test - void when_adminRole_then_updateUser() throws Exception { - UserRequestAdmin request = new UserRequestAdmin( - "updatedUser", - "Updated Name", - "updated@test.com", - "newPassword", - Role.ADMIN - ); - - UserResponse response = new UserResponse( - 1L, - "updatedUser", - "Updated Name", - "updated@test.com", - Role.ADMIN - ); - - - mockMvc.perform(put("/api/users/admin/1") - .with(user(customUserDetails)) + void updateUser_whenAdminRole_shouldReturnUpdatedUser() throws Exception { + mockMvc.perform(put(USER_PATH_ID, 1L) + .with(user(adminCustomUserDetails)) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) + .content(objectMapper.writeValueAsString(updateUserRequestAdmin)) + .param("role", Role.ADMIN.name())) .andExpect(status().isOk()) .andExpect(jsonPath("$.username").value("updatedUser")) .andExpect(jsonPath("$.email").value("updated@test.com")) @@ -136,39 +128,49 @@ void when_adminRole_then_updateUser() throws Exception { } @Test - void when_notAdminRole_then_forbidden() throws Exception { - UserRequestAdmin request = new UserRequestAdmin( - "updatedUser", - "Updated Name", - "updated@test.com", - "newPassword", - Role.ADMIN - ); - - mockMvc.perform(put("/api/users/admin/1") - .with(user("user").roles("USER")) + void updateUser_whenNotAdminRole_shouldReturnForbidden() throws Exception { + mockMvc.perform(put(USER_PATH_ID, 1L) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) + .content(objectMapper.writeValueAsString(updateUserRequestAdmin)) + .with(user(userCustomUserDetails)) + .param("role", Role.ADMIN.name())) .andExpect(status().isForbidden()); } + + @Test + void updateUser_whenNoAuthentication_shouldReturnUnauthorized() throws Exception { + mockMvc.perform(put(USER_PATH_ID, 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updateUserRequestAdmin)) + .param("role", Role.ADMIN.name())) + .andExpect(status().isUnauthorized()); + } } - @Nested - class DeleteUserTest { - - @Test - void when_adminRole_then_deleteUser() throws Exception { - mockMvc.perform(delete("/api/users/admin/{id}",99L) - .with(user(customUserDetails)) - .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isNoContent()); - } - @Test - void when_notAdminRole_then_forbidden() throws Exception { - mockMvc.perform(delete("/api/users/admin/1") - .with(user("user").roles("USER")) - .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isForbidden()); - } - } + @Nested + class DeleteUserTest { + + @Test + void deleteUser_whenAdminRole_shouldDeleteUser() throws Exception { + mockMvc.perform(delete(USER_PATH_ID, 99L) + .with(user(adminCustomUserDetails)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + } + + @Test + void deleteUser_whenNotAdminRole_shouldReturnForbidden() throws Exception { + mockMvc.perform(delete(USER_PATH_ID, 1L) + .with(user(userCustomUserDetails)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } + + @Test + void deleteUser_whenNoAuthentication_shouldReturnUnauthorized() throws Exception { + mockMvc.perform(delete(USER_PATH_ID, 1L) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnauthorized()); + } + } } diff --git a/src/test/java/com/SleepUp/SU/user/admin/UserAdminServiceImplTest.java b/src/test/java/com/SleepUp/SU/user/admin/UserAdminServiceImplTest.java index 57f5286e..2b68d04d 100644 --- a/src/test/java/com/SleepUp/SU/user/admin/UserAdminServiceImplTest.java +++ b/src/test/java/com/SleepUp/SU/user/admin/UserAdminServiceImplTest.java @@ -9,6 +9,7 @@ import com.SleepUp.SU.user.dto.UserResponse; import com.SleepUp.SU.user.role.Role; import com.SleepUp.SU.user.utils.UserServiceHelper; +import com.SleepUp.SU.utils.exceptions.UserNotFoundByIdException; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -53,7 +54,7 @@ public class UserAdminServiceImplTest { class LoadUserTest { @Test - void should_loadExistingUser_fromRequest() { + void loadUserByUsername_existingUser_shouldReturnUserDetails() { UserRequest userRequest = new UserRequest("userTest", "nameTest", "usertest@test.com", "password123"); User userSaved = new User(); userSaved.setId(1L); @@ -62,7 +63,7 @@ void should_loadExistingUser_fromRequest() { userSaved.setEmail("usertest@email.com"); userSaved.setPassword("password123"); - when(userServiceHelper.findByUsername("userTest")).thenReturn(userSaved); + when(userServiceHelper.getUserEntityByUsername("userTest")).thenReturn(userSaved); List authorities = List.of( new SimpleGrantedAuthority("ROLE_USER") @@ -81,9 +82,9 @@ void should_loadExistingUser_fromRequest() { } @Test - void should_loadExistingUser_throw_exception() { + void loadUserByUsername_nonExistingUser_shouldThrowException() { - when(userServiceHelper.findByUsername("userTest")) + when(userServiceHelper.getUserEntityByUsername("userTest")) .thenThrow(new UsernameNotFoundException("userTest does not exist.")); assertThrows(UsernameNotFoundException.class, () -> userAdminServiceImpl.loadUserByUsername("userTest")); @@ -93,37 +94,41 @@ void should_loadExistingUser_throw_exception() { @Nested class UpdateUserTest { - @Test - void should_updateExistingUser() { - UserRequestAdmin request = new UserRequestAdmin( - "updatedUser", - "Updated Name", - "updated@test.com", - "newPassword", - Role.ADMIN - ); - - User existingUser = new User(); - existingUser.setId(1L); - existingUser.setUsername("oldUser"); - existingUser.setName("Old Name"); - existingUser.setEmail("old@test.com"); - existingUser.setPassword("oldPassword"); - existingUser.setRole(Role.USER); - when(userRepository.findById(1L)).thenReturn(Optional.of(existingUser)); - - User savedUser = new User(); - savedUser.setId(1L); - savedUser.setUsername("updateUser"); - savedUser.setName("update Name"); - savedUser.setEmail("updated@test.com"); - savedUser.setPassword("encodedPass"); - savedUser.setRole(Role.ADMIN); - - when(userRepository.save(any(User.class))).thenReturn(savedUser); - when(userMapper.toResponse(any(User.class))) - .thenReturn(new UserResponse(1L, "updatedUser", "Updated Name", "updated@test.com", Role.ADMIN)); + @Test + void updateUser_existingUser_shouldReturnUpdatedUser() { + + UserRequestAdmin request = UserRequestAdmin.builder() + .username("updatedUser") + .name("Updated Name") + .email("updated@test.com") + .password("newPassword") + .role(Role.ADMIN) + .build(); + + User existingUser = User.builder() + .id(1L) + .username("oldUser") + .name("Old Name") + .email("old@test.com") + .password("oldPassword") + .role(Role.USER) + .build(); + + User updatedUser = User.builder() + .id(1L) + .username("updatedUser") + .name("Updated Name") + .email("updated@test.com") + .password("encodedPass") + .role(Role.ADMIN) + .build(); + + UserResponse userResponse = new UserResponse(1L, "updatedUser", "Updated Name", "updated@test.com", Role.ADMIN); + + when(userServiceHelper.getUserEntityById(1L)).thenReturn(existingUser); + when(userServiceHelper.updateUserDataAdmin(eq(request), any(User.class))).thenReturn(updatedUser); + when(userMapper.toResponse(any(User.class))).thenReturn(userResponse); UserResponse response = userAdminServiceImpl.updateUser(1L, request); @@ -132,64 +137,7 @@ void should_updateExistingUser() { assertEquals("updated@test.com", response.email()); assertEquals(Role.ADMIN, response.role()); - verify(userRepository, times(1)).save(any(User.class)); - } - @Test - void should_throwException_when_userNotFound() { - when(userRepository.findById(1L)).thenReturn(Optional.empty()); - - UserRequestAdmin request = new UserRequestAdmin( - "updatedUser", - "Updated Name", - "updated@test.com", - "password", - Role.ADMIN - ); - - assertThrows(RuntimeException.class, () -> userAdminServiceImpl.updateUser(1L, request)); - - -// existingUser.setPassword("oldPass"); - // existingUser.setRole(Role.USER); -// - // UserRequestAdmin updateRequest = new UserRequestAdmin( -// "newUsername", -// "New Name", -// "new@test.com", -// "newPass123", -// Role.ADMIN -// ); -// -// User updatedUser = new User(); -// updatedUser.setId(1L); -// updatedUser.setUsername("newUsername"); -// updatedUser.setName("Old Name"); -// updatedUser.setEmail("new@email.com"); -// updatedUser.setPassword("newPass123"); -// updatedUser.setRole(Role.USER); -// -// UserResponse updatedUserResponse = new UserResponse(updatedUser.getId(), updatedUser.getUsername(), updatedUser.getName(), updatedUser.getEmail(), updatedUser.getRole()); -// -// -// when(userServiceHelper.findById(1L)).thenReturn(existingUser); -// doAnswer(invocation -> { -// existingUser.setUsername("newUsername"); -// existingUser.setEmail("new@email.com"); -// existingUser.setPassword("encodedPassword"); -// return null; -// }).when(userServiceHelper).updateUserDataAdmin(updateRequest, existingUser); -// -// when(userRepository.save(existingUser)).thenReturn(updatedUser); -// when(userMapper.toResponse(updatedUser)).thenReturn(updatedUserResponse); -// -// UserResponse response = userAdminServiceImpl.updateUser(1L, updateRequest); -// -// assertEquals("newUsername", response.username()); -// assertEquals("new@email.com", response.email()); -// assertEquals("Old Name", response.name()); -//>>>>>>> dev - } } } diff --git a/src/test/java/com/SleepUp/SU/user/user/UserUserControllerTest.java b/src/test/java/com/SleepUp/SU/user/user/UserUserControllerTest.java index 04d820f7..be686078 100644 --- a/src/test/java/com/SleepUp/SU/user/user/UserUserControllerTest.java +++ b/src/test/java/com/SleepUp/SU/user/user/UserUserControllerTest.java @@ -3,45 +3,40 @@ import com.SleepUp.SU.user.entity.User; import com.SleepUp.SU.user.repository.UserRepository; import com.SleepUp.SU.user.dto.UserRequest; -import com.SleepUp.SU.user.role.Role; +import com.SleepUp.SU.user.entity.CustomUserDetails; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; -import com.SleepUp.SU.user.entity.CustomUserDetails; -import com.SleepUp.SU.user.dto.UserResponse; -import org.junit.jupiter.api.Test; -import org.springframework.http.MediaType; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - - @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) public class UserUserControllerTest { + public static final String BASE = "/users"; + public static final String LOGGED_USER = BASE + "/me"; + @Autowired private MockMvc mockMvc; @Autowired private UserUserController userUserController; - @MockBean + @Autowired private UserUserServiceImpl userUserServiceImpl; @Autowired @@ -54,49 +49,44 @@ public class UserUserControllerTest { @BeforeEach void setUp() { - User savedUser = userRepository.findByUsername("TestUser").get(); + User savedUser = userRepository.findByUsername("TestUser") + .orElseThrow(() -> new RuntimeException("TestUser not found")); customUserDetails = new CustomUserDetails(savedUser); } @Nested - class getLoggedUser{ + class getLoggedUser { @Test - void when_authenticated_then_return_logged_user() throws Exception { - - UserResponse response = new UserResponse(99L, "testUser", "Test Name", "test@email.com", Role.USER); - - when(userUserServiceImpl.getLoggedUser(anyLong())).thenReturn(response); - - mockMvc.perform(get("/api/users/me") + void getLoggedUser_whenUserIsAuthenticated_shouldReturnCurrentUserInfo() throws Exception { + mockMvc.perform(get(LOGGED_USER) .with(user(customUserDetails)) .accept(MediaType.APPLICATION_JSON)) + .andDo(print()) .andExpect(status().isOk()) - .andExpect(jsonPath("$.username").value("testUser")) - .andExpect(jsonPath("$.name").value("Test Name")) - .andExpect(jsonPath("$.email").value("test@email.com")); + .andExpect(jsonPath("$.username").value("TestUser")) + .andExpect(jsonPath("$.name").value("nameTest")) + .andExpect(jsonPath("$.email").value("usertnest@test.com")) + .andExpect(jsonPath("$.role").value("USER")); } @Test - void when_not_authenticated_then_return_unauthorized() throws Exception { - mockMvc.perform(get("/api/users/me") + void getLoggedUser_whenNotAuthenticated_shouldReturnUnauthorized() throws Exception { + mockMvc.perform(get(LOGGED_USER) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isUnauthorized()); } } @Nested - class updateLoggedUser{ + class updateLoggedUser { @Test - void when_authenticated_then_update_logged_user() throws Exception { + void updateLoggedUser_whenAuthenticated_shouldUpdateAndReturnUpdatedUser() throws Exception { UserRequest request = new UserRequest("updatedUser", "updateName", "updated@email.com", "newPassword"); - UserResponse response = new UserResponse(99L, "updatedUser", "updateName", "updated@email.com", Role.USER); - - when(userUserServiceImpl.updateLoggedUser(any(UserRequest.class), anyLong())).thenReturn(response); mockMvc.perform( - put("/api/users/me") + put(LOGGED_USER) .with(user(customUserDetails)) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) @@ -109,17 +99,17 @@ void when_authenticated_then_update_logged_user() throws Exception { } @Test - void when_not_authenticated_then_update_fails_with_unauthorized() throws Exception { + void updateLoggedUser_whenNotAuthenticated_shouldReturnUnauthorized() throws Exception { String jsonRequest = """ - { - "username": "updatedUser", - "email": "updated@email.com", - "password": "newPassword" - } - """; + { + "username": "updatedUser", + "email": "updated@email.com", + "password": "newPassword" + } + """; mockMvc.perform( - put("/api/users/me") + put(LOGGED_USER) .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest) .accept(MediaType.APPLICATION_JSON) @@ -128,27 +118,23 @@ void when_not_authenticated_then_update_fails_with_unauthorized() throws Excepti } @Test - void when_service_throws_exception_then_return_bad_request() throws Exception { - UserRequest request = new UserRequest("updatedUser", "updateName", "updated@email.com", "newPassword"); - - when(userUserServiceImpl.updateLoggedUser(request, 99L)) - .thenThrow(new IllegalArgumentException("Invalid update")); - + void updateLoggedUser_whenServiceThrowsException_shouldReturnBadRequest() throws Exception { String jsonRequest = """ - { - "username": "updatedUser", - "email": "updated@email.com", - "password": "newPassword" - } - """; + { + "username": "updatedUser", + "email": "updated@email.com", + "password": "newPassword" + } + """; mockMvc.perform( - put("/api/users/me") + put(LOGGED_USER) .with(user(customUserDetails)) .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest) .accept(MediaType.APPLICATION_JSON) ) + .andDo(print()) .andExpect(status().isBadRequest()); } } @@ -157,8 +143,8 @@ void when_service_throws_exception_then_return_bad_request() throws Exception { class deleteLoggedUser { @Test - void when_authenticated_then_delete_logged_user() throws Exception { - mockMvc.perform(delete("/api/users/me") + void deleteLoggedUser_whenAuthenticated_shouldDeleteAccountAndReturnSuccess() throws Exception { + mockMvc.perform(delete(LOGGED_USER) .with(user(customUserDetails)) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) @@ -166,8 +152,8 @@ void when_authenticated_then_delete_logged_user() throws Exception { } @Test - void when_not_authenticated_then_delete_fails_with_unauthorized() throws Exception { - mockMvc.perform(delete("/api/users/me") + void deleteLoggedUser_whenNotAuthenticated_shouldReturnUnauthorized() throws Exception { + mockMvc.perform(delete(LOGGED_USER) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isUnauthorized()); } diff --git a/src/test/java/com/SleepUp/SU/user/user/UserUserServiceImplTest.java b/src/test/java/com/SleepUp/SU/user/user/UserUserServiceImplTest.java index 324affb3..12eb5a9d 100644 --- a/src/test/java/com/SleepUp/SU/user/user/UserUserServiceImplTest.java +++ b/src/test/java/com/SleepUp/SU/user/user/UserUserServiceImplTest.java @@ -52,11 +52,11 @@ public class UserUserServiceImplTest { class getLoggedUser{ @Test - void when_getLoggesUser_return_loggedUser(){ + void getLoggedUser_whenUserExists_shouldReturnLoggedUser(){ User user = new User(99L,"usernameTest", "nameTest", "email@test.com", "testPassword", Role.USER, null, null); UserResponse userResponse = new UserResponse(99L,"usernameTest", "nameTest", "email@test.com", Role.USER); - when(userServiceHelper.findById(99L)).thenReturn(user); + when(userServiceHelper.getUserEntityById(99L)).thenReturn(user); when(userMapper.toResponse(user)).thenReturn(userResponse); UserResponse response = userUserServiceImpl.getLoggedUser(99L); @@ -67,8 +67,8 @@ void when_getLoggesUser_return_loggedUser(){ } @Test - void when_getLoggedUser_throw_exception() { - when(userServiceHelper.findById(99L)) + void getLoggedUser_whenUserNotFound_shouldThrowException() { + when(userServiceHelper.getUserEntityById(99L)) .thenThrow(new IllegalArgumentException("Username by id does not exist")); IllegalArgumentException exception = assertThrows( @@ -85,14 +85,14 @@ void when_getLoggedUser_throw_exception() { class updateLoggedUser { @Test - void when_updateLoggedUser_with_new_data_then_return_updated_response() { + void updateLoggedUser_whenValidRequest_shouldReturnUpdatedUser() { User user = new User(99L, "oldUsername", "oldName", "old@email.com", "oldPassword", Role.USER, null, null); UserRequest request = new UserRequest("newUsername", "","new@email.com", "newPassword"); User updatedUser = new User(99L, "newUsername", "oldName", "new@email.com", "encodedPassword", Role.USER, null, null); UserResponse expectedResponse = new UserResponse(99L, "newUsername", "oldName", "new@email.com", Role.USER); - when(userServiceHelper.findById(99L)).thenReturn(user); + when(userServiceHelper.getUserEntityById(99L)).thenReturn(user); doAnswer(invocation -> { user.setUsername("newUsername"); user.setEmail("new@email.com"); @@ -109,13 +109,13 @@ void when_updateLoggedUser_with_new_data_then_return_updated_response() { } @Test - void when_updateLoggedUser_with_empty_request_then_return_same_response() { + void updateLoggedUser_whenEmptyRequest_shouldReturnSameUser() { User user = new User(99L, "sameUsername", "sameName", "same@email.com", "samePassword", Role.USER, null, null); UserRequest request = new UserRequest("", "", "", ""); UserResponse expectedResponse = new UserResponse(99L, "sameUsername", "sameName", "same@email.com", Role.USER); - when(userServiceHelper.findById(99L)).thenReturn(user); + when(userServiceHelper.getUserEntityById(99L)).thenReturn(user); doAnswer(invocation -> { return null; }).when(userServiceHelper).updateUserData(request, user); @@ -129,10 +129,10 @@ void when_updateLoggedUser_with_empty_request_then_return_same_response() { } @Test - void when_updateLoggedUser_user_not_found_then_throw_exception() { + void updateLoggedUser_whenUserNotFound_shouldThrowException() { UserRequest request = new UserRequest("any", "any", "any", "any"); - when(userServiceHelper.findById(99L)) + when(userServiceHelper.getUserEntityById(99L)) .thenThrow(new IllegalArgumentException("User not found")); assertThrows(IllegalArgumentException.class, () -> { @@ -144,11 +144,11 @@ void when_updateLoggedUser_user_not_found_then_throw_exception() { @Nested class deleteLoggedUser { @Test - void when_user_exists_then_delete_successfully() { + void deleteLoggedUser_whenUserExists_shouldDeleteSuccessfully() { User user = new User(99L,"usernameTest", "nameTest", "email@test.com", "testPassword", Role.USER, null, null); User user1 = new User(1L,"usernameTest1", "nameTest1", "email1@test.com", "testPassword", Role.USER, null, null); - when(userServiceHelper.findById(99L)).thenReturn(user); + when(userServiceHelper.getUserEntityById(99L)).thenReturn(user); when(userRepository.findById(1L)).thenReturn(Optional.of(user1)); when(accommodationRepository.findByManagedBy_Id(99L)).thenReturn(List.of()); diff --git a/src/test/java/com/SleepUp/SU/user/utils/UserServiceHelperTest.java b/src/test/java/com/SleepUp/SU/user/utils/UserServiceHelperTest.java index 4f6cf827..c9d0e1a1 100644 --- a/src/test/java/com/SleepUp/SU/user/utils/UserServiceHelperTest.java +++ b/src/test/java/com/SleepUp/SU/user/utils/UserServiceHelperTest.java @@ -37,16 +37,16 @@ public class UserServiceHelperTest { class findById{ @Test - void when_checkUserId_return_user() { + void findByUserId_validId_shouldReturnUser() { User user = new User(); user.setId(99L); user.setUsername("test"); when(userRepository.findById(99L)).thenReturn(Optional.of(user)); - User result = userServiceHelper.findById(99L); + User result = userServiceHelper.getUserEntityById(99L); assertNotNull(result); - assertDoesNotThrow(() ->userServiceHelper.findById(99L)); + assertDoesNotThrow(() ->userServiceHelper.getUserEntityById(99L)); assertEquals(99L, result.getId()); assertEquals("test", result.getUsername()); } @@ -57,7 +57,7 @@ class validateUserDoesNotExist { @Test @MockitoSettings(strictness = Strictness.LENIENT) - void when_validateUserDoesNotExist_then_noExceptionThrow() { + void validateUserDoesNotExist_whenNoConflict_shouldPass() { when(userRepository.existsByUsername("testUser")). thenReturn(false); when(userRepository.existsByUsername("testUser@email.com")). thenReturn(false); @@ -65,7 +65,7 @@ void when_validateUserDoesNotExist_then_noExceptionThrow() { } @Test - void when_username_exists_then_throw_exception() { + void validateUserDoesNotExist_whenUsernameExists_shouldThrowException() { when(userRepository.existsByUsername("testUser")).thenReturn(true); UserUsernameAlreadyExistsException exception = assertThrows( @@ -77,7 +77,7 @@ void when_username_exists_then_throw_exception() { } @Test - void when_email_exists_then_throw_exception() { + void validateUserDoesNotExist_whenEmailExists_shouldThrowException() { when(userRepository.existsByUsername("testUser")).thenReturn(false); when(userRepository.existsByEmail("testUser@email.com")).thenReturn(true); diff --git a/src/test/java/com/SleepUp/SU/utils/EmailServiceHelperTest.java b/src/test/java/com/SleepUp/SU/utils/EmailServiceHelperTest.java new file mode 100644 index 00000000..23e9dcc8 --- /dev/null +++ b/src/test/java/com/SleepUp/SU/utils/EmailServiceHelperTest.java @@ -0,0 +1,326 @@ +package com.SleepUp.SU.utils; + +import com.SleepUp.SU.accommodation.entity.Accommodation; +import com.SleepUp.SU.config.properties.MailProperties; +import com.SleepUp.SU.reservation.entity.Reservation; +import com.SleepUp.SU.user.entity.User; +import com.SleepUp.SU.utils.email.EmailService; +import com.SleepUp.SU.utils.email.EmailServiceHelper; +import jakarta.mail.MessagingException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +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 java.math.BigDecimal; +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class EmailServiceHelperTest { + + @Mock + private EmailService emailService; + + @Mock + private MailProperties mailProperties; + + @InjectMocks + private EmailServiceHelper emailServiceHelper; + + private User guest; + private User owner; + private Accommodation accommodation; + private Reservation reservation; + + @BeforeEach + void setUp() { + guest = new User(); + guest.setEmail("guest@test.com"); + guest.setName("John Doe"); + + owner = new User(); + owner.setEmail("owner@test.com"); + owner.setName("Jane Smith"); + + accommodation = new Accommodation(); + accommodation.setName("Cozy Apartment"); + accommodation.setLocation("Madrid, Spain"); + accommodation.setManagedBy(owner); + + reservation = new Reservation(); + reservation.setUser(guest); + reservation.setAccommodation(accommodation); + reservation.setCheckInDate(LocalDate.of(2025, 10, 1)); + reservation.setCheckOutDate(LocalDate.of(2025, 10, 5)); + reservation.setTotalPrice(BigDecimal.valueOf(100)); + } + + @Nested + class ValidateEmailConfigGroup { + @Test + void validateEmailConfig_validConfig_shouldEnableEmail() { + when(mailProperties.getFrom()).thenReturn("from@test.com"); + when(mailProperties.getUsername()).thenReturn("username"); + when(mailProperties.getPassword()).thenReturn("password"); + + emailServiceHelper.mailProperties = mailProperties; + + emailServiceHelper.validateEmailConfig(); + } + + @Test + void validateEmailConfig_missingFields_shouldDisableEmail() { + when(mailProperties.getFrom()).thenReturn(null); + + emailServiceHelper.mailProperties = mailProperties; + + emailServiceHelper.validateEmailConfig(); + } + } + + @Nested + class SendWelcomeEmailGroup { + @Test + void sendWelcomeEmail_Success() throws MessagingException { + doNothing().when(emailService).sendWelcomeEmail(any(User.class)); + + emailServiceHelper.sendWelcomeEmail(guest); + + verify(emailService).sendWelcomeEmail(guest); + } + + @Test + void sendWelcomeEmail_HandlesException() throws MessagingException { + doThrow(new MessagingException("Error")).when(emailService).sendWelcomeEmail(any(User.class)); + + assertDoesNotThrow(() -> emailServiceHelper.sendWelcomeEmail(guest)); + + verify(emailService).sendWelcomeEmail(guest); + } + } + + + @Nested + class SendOwnerReservedNotificationGroup { + @Test + void sendOwnerReservedNotification_Success() throws MessagingException { + doNothing().when(emailService).sendOwnerReservedNotification(any(Reservation.class)); + + emailServiceHelper.sendOwnerReservedNotification(reservation); + + verify(emailService).sendOwnerReservedNotification(reservation); + } + } + + @Nested + class SendReservationConfirmationEmailGroup { + @Test + void sendReservationConfirmationEmail_Success() throws MessagingException { + doNothing().when(emailService).sendGuestReservationConfirmationEmail(any(Reservation.class), any(BigDecimal.class)); + + emailServiceHelper.sendReservationConfirmationEmail(reservation, BigDecimal.TEN); + + verify(emailService).sendGuestReservationConfirmationEmail(reservation, BigDecimal.TEN); + } + } + + @Nested + class SendReservationReminderEmailGroup { + @Test + void sendReservationReminderEmail_Success() throws MessagingException { + doNothing().when(emailService).sendReservationReminderEmail(any(Reservation.class)); + + emailServiceHelper.sendReservationReminderEmail(reservation); + + verify(emailService).sendReservationReminderEmail(reservation); + } + } + + @Nested + class SendOwnerReservationReminderEmailGroup { + @Test + void sendOwnerReservationReminderEmail_Success() throws MessagingException { + doNothing().when(emailService).sendOwnerReservationReminderEmail(any(Reservation.class)); + + emailServiceHelper.sendOwnerReservationReminderEmail(reservation); + + verify(emailService).sendOwnerReservationReminderEmail(reservation); + } + } + + @Nested + class SendCancellationConfirmationEmailGroup { + @Test + void sendCancellationConfirmationEmail_Success() throws MessagingException { + doNothing().when(emailService).sendCancellationConfirmationEmail(any(Reservation.class)); + + emailServiceHelper.sendCancellationConfirmationEmail(reservation); + + verify(emailService).sendCancellationConfirmationEmail(reservation); + } + } + + @Nested + class SendCancellationByOwnerNotificationEmailGroup { + @Test + void sendCancellationByOwnerNotificationEmail_Success() throws MessagingException { + doNothing().when(emailService).sendCancellationByOwnerNotificationEmail(any(Reservation.class)); + + emailServiceHelper.sendCancellationByOwnerNotificationEmail(reservation); + + verify(emailService).sendCancellationByOwnerNotificationEmail(reservation); + } + } + + @Nested + class SendCancellationNotificationToOwnerEmailGroup { + @Test + void sendCancellationNotificationToOwnerEmail_Success() throws MessagingException { + doNothing().when(emailService).sendCancellationNotificationToOwnerEmail(any(Reservation.class)); + + emailServiceHelper.sendCancellationNotificationToOwnerEmail(reservation); + + verify(emailService).sendCancellationNotificationToOwnerEmail(reservation); + } + } + + @Nested + class HandleNewReservationEmailsGroup { + @Test + void handleNewReservationEmails_SendsBothEmails() throws MessagingException { + doNothing().when(emailService).sendGuestReservationConfirmationEmail(any(Reservation.class), any(BigDecimal.class)); + doNothing().when(emailService).sendOwnerReservedNotification(any(Reservation.class)); + + emailServiceHelper.handleNewReservationEmails(reservation, BigDecimal.valueOf(3)); + + verify(emailService).sendGuestReservationConfirmationEmail(reservation, BigDecimal.valueOf(3)); + verify(emailService).sendOwnerReservedNotification(reservation); + } + } + + @Nested + class HandleGuestCancellationEmailsGroup { + @Test + void handleGuestCancellationEmails_SendsBothEmails() throws MessagingException { + doNothing().when(emailService).sendCancellationConfirmationEmail(any(Reservation.class)); + doNothing().when(emailService).sendCancellationNotificationToOwnerEmail(any(Reservation.class)); + + emailServiceHelper.handleGuestCancellationEmails(reservation); + + verify(emailService).sendCancellationConfirmationEmail(reservation); + verify(emailService).sendCancellationNotificationToOwnerEmail(reservation); + } + } + + @Nested + class HandleOwnerCancellationEmailsGroup { + @Test + void handleOwnerCancellationEmails_SendsNotification() throws MessagingException { + doNothing().when(emailService).sendCancellationByOwnerNotificationEmail(any(Reservation.class)); + + emailServiceHelper.handleOwnerCancellationEmails(reservation); + + verify(emailService).sendCancellationByOwnerNotificationEmail(reservation); + } + } + + @Nested + class SendReservationRemindersGroup { + @Test + void sendReservationReminders_SendsBothReminders() throws MessagingException { + doNothing().when(emailService).sendReservationReminderEmail(any(Reservation.class)); + doNothing().when(emailService).sendOwnerReservationReminderEmail(any(Reservation.class)); + + emailServiceHelper.sendReservationReminders(reservation); + + verify(emailService).sendReservationReminderEmail(reservation); + verify(emailService).sendOwnerReservationReminderEmail(reservation); + } + } + + @Nested + class CanSendEmailsGroup { + @Test + void canSendEmails_WithValidUser_ReturnsTrue() { + boolean result = emailServiceHelper.canSendEmails(guest); + + assertTrue(result); + } + + @Test + void canSendEmails_WithNullUser_ReturnsFalse() { + assertFalse(emailServiceHelper.canSendEmails(null)); + } + + @Test + void canSendEmails_WithNullEmail_ReturnsFalse() { + guest.setEmail(null); + assertFalse(emailServiceHelper.canSendEmails(guest)); + } + + @Test + void canSendEmails_WithEmptyEmail_ReturnsFalse() { + guest.setEmail(" "); + assertFalse(emailServiceHelper.canSendEmails(guest)); + } + } + + @Nested + class CanSendReservationEmailsGroup { + @Test + void canSendReservationEmails_WithValidData_ReturnsTrue() { + assertTrue(emailServiceHelper.canSendReservationEmails(reservation)); + } + + @Test + void canSendReservationEmails_WithInvalidGuest_ReturnsFalse() { + guest.setEmail(null); + assertFalse(emailServiceHelper.canSendReservationEmails(reservation)); + } + + @Test + void canSendReservationEmails_WithNullAccommodation_ReturnsFalse() { + reservation.setAccommodation(null); + assertFalse(emailServiceHelper.canSendReservationEmails(reservation)); + } + + @Test + void canSendReservationEmails_WithNullOwner_ReturnsFalse() { + accommodation.setManagedBy(null); + assertFalse(emailServiceHelper.canSendReservationEmails(reservation)); + } + + @Test + void canSendReservationEmails_WithNullReservation_ReturnsFalse() { + assertFalse(emailServiceHelper.canSendReservationEmails(null)); + } + + @Test + void canSendReservationEmails_WithInvalidOwnerEmail_ReturnsFalse() { + owner.setEmail(""); + assertFalse(emailServiceHelper.canSendReservationEmails(reservation)); + } + } + + @Nested + class ExceptionHandlingGroup { + @Test + void allEmailMethods_HandleExceptionsGracefully() throws MessagingException { + doThrow(new MessagingException("Error")).when(emailService) + .sendGuestReservationConfirmationEmail(any(Reservation.class), any(BigDecimal.class)); + doThrow(new MessagingException("Error")).when(emailService) + .sendOwnerReservedNotification(any(Reservation.class)); + + assertDoesNotThrow(() -> + emailServiceHelper.handleNewReservationEmails(reservation, BigDecimal.valueOf(3)) + ); + } + } +} diff --git a/src/test/java/utils/EmailServiceImplTest.java b/src/test/java/com/SleepUp/SU/utils/EmailServiceImplTest.java similarity index 76% rename from src/test/java/utils/EmailServiceImplTest.java rename to src/test/java/com/SleepUp/SU/utils/EmailServiceImplTest.java index 8570f9db..35d9d79f 100644 --- a/src/test/java/utils/EmailServiceImplTest.java +++ b/src/test/java/com/SleepUp/SU/utils/EmailServiceImplTest.java @@ -1,5 +1,4 @@ -package utils; - +package com.SleepUp.SU.utils; import com.SleepUp.SU.accommodation.entity.Accommodation; import com.SleepUp.SU.reservation.entity.Reservation; @@ -17,9 +16,9 @@ import org.thymeleaf.context.Context; import org.thymeleaf.spring6.SpringTemplateEngine; +import java.math.BigDecimal; import java.time.LocalDate; -import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -61,6 +60,8 @@ void setUp() { reservation = new Reservation(); reservation.setCheckInDate(LocalDate.of(2025, 10, 1)); reservation.setCheckOutDate(LocalDate.of(2025, 10, 5)); + reservation.setUser(guest); + reservation.setAccommodation(accommodation); } @Test @@ -69,7 +70,7 @@ void sendWelcomeEmail_Success() throws MessagingException { when(templateEngine.process(eq("WelcomeUser"), any(Context.class))) .thenReturn("Welcome Email"); - emailService.sendWelcomeEmail("test@test.com", "TestUser"); + emailService.sendWelcomeEmail(guest); verify(mailSender).createMimeMessage(); verify(mailSender).send(mimeMessage); @@ -82,7 +83,7 @@ void sendOwnerReservedNotification_Success() throws MessagingException { when(templateEngine.process(eq("NotificationReservationOwner"), any(Context.class))) .thenReturn("Owner Notification"); - emailService.sendOwnerReservedNotification(guest, accommodation, reservation); + emailService.sendOwnerReservedNotification(reservation); verify(mailSender).createMimeMessage(); verify(mailSender).send(mimeMessage); @@ -90,31 +91,12 @@ void sendOwnerReservedNotification_Success() throws MessagingException { } @Test - void sendReservationConfirmationEmail_WithStringParams_Success() throws MessagingException { - when(mailSender.createMimeMessage()).thenReturn(mimeMessage); - when(templateEngine.process(eq("ConfirmationGuest"), any(Context.class))) - .thenReturn("Confirmation"); - - emailService.sendReservationConfirmationEmail( - "guest@test.com", - "John", - "Apartment", - "Madrid", - "2025-10-01", - "2025-10-05" - ); - - verify(mailSender).send(mimeMessage); - verify(templateEngine).process(eq("ConfirmationGuest"), any(Context.class)); - } - - @Test - void sendReservationConfirmationEmail_WithEntities_Success() throws MessagingException { + void sendReservationConfirmationEmail_Success() throws MessagingException { when(mailSender.createMimeMessage()).thenReturn(mimeMessage); when(templateEngine.process(eq("ConfirmationGuest"), any(Context.class))) .thenReturn("Confirmation"); - emailService.sendReservationConfirmationEmail(guest, accommodation, reservation); + emailService.sendGuestReservationConfirmationEmail(reservation, BigDecimal.valueOf(3)); verify(mailSender).send(mimeMessage); verify(templateEngine).process(eq("ConfirmationGuest"), any(Context.class)); @@ -126,7 +108,7 @@ void sendReservationReminderEmail_Success() throws MessagingException { when(templateEngine.process(eq("ReminderGuestReservation"), any(Context.class))) .thenReturn("Reminder"); - emailService.sendReservationReminderEmail(guest, accommodation, reservation); + emailService.sendReservationReminderEmail(reservation); verify(mailSender).send(mimeMessage); verify(templateEngine).process(eq("ReminderGuestReservation"), any(Context.class)); @@ -138,7 +120,7 @@ void sendOwnerReservationReminderEmail_Success() throws MessagingException { when(templateEngine.process(eq("ReminderOwnerReservation"), any(Context.class))) .thenReturn("Owner Reminder"); - emailService.sendOwnerReservationReminderEmail(accommodation, reservation); + emailService.sendOwnerReservationReminderEmail(reservation); verify(mailSender).send(mimeMessage); verify(templateEngine).process(eq("ReminderOwnerReservation"), any(Context.class)); @@ -150,7 +132,7 @@ void sendCancellationConfirmationEmail_Success() throws MessagingException { when(templateEngine.process(eq("CancellationGuest"), any(Context.class))) .thenReturn("Cancellation"); - emailService.sendCancellationConfirmationEmail(guest, accommodation, reservation); + emailService.sendCancellationConfirmationEmail(reservation); verify(mailSender).send(mimeMessage); verify(templateEngine).process(eq("CancellationGuest"), any(Context.class)); @@ -162,7 +144,7 @@ void sendCancellationByOwnerNotificationEmail_Success() throws MessagingExceptio when(templateEngine.process(eq("CancellationGuestByOwner"), any(Context.class))) .thenReturn("Cancellation by Owner"); - emailService.sendCancellationByOwnerNotificationEmail(guest, accommodation, reservation); + emailService.sendCancellationByOwnerNotificationEmail(reservation); verify(mailSender).send(mimeMessage); verify(templateEngine).process(eq("CancellationGuestByOwner"), any(Context.class)); @@ -174,7 +156,7 @@ void sendCancellationNotificationToOwnerEmail_Success() throws MessagingExceptio when(templateEngine.process(eq("CancellationByGuestOwner"), any(Context.class))) .thenReturn("Cancellation to Owner"); - emailService.sendCancellationNotificationToOwnerEmail(guest, accommodation, reservation); + emailService.sendCancellationNotificationToOwnerEmail(reservation); verify(mailSender).send(mimeMessage); verify(templateEngine).process(eq("CancellationByGuestOwner"), any(Context.class)); @@ -186,7 +168,7 @@ void allEmailMethods_UseCorrectEncoding() throws MessagingException { when(templateEngine.process(anyString(), any(Context.class))) .thenReturn("Email"); - emailService.sendWelcomeEmail("test@test.com", "Test"); + emailService.sendWelcomeEmail(guest); verify(mailSender).createMimeMessage(); } @@ -197,10 +179,10 @@ void allEmailMethods_SetCorrectSubjects() throws MessagingException { when(templateEngine.process(anyString(), any(Context.class))) .thenReturn("Email"); - emailService.sendWelcomeEmail("test@test.com", "Test"); - emailService.sendReservationConfirmationEmail(guest, accommodation, reservation); - emailService.sendCancellationConfirmationEmail(guest, accommodation, reservation); + emailService.sendWelcomeEmail(guest); + emailService.sendGuestReservationConfirmationEmail(reservation, BigDecimal.valueOf(3)); + emailService.sendCancellationConfirmationEmail(reservation); verify(mailSender, times(3)).send(mimeMessage); } -} \ No newline at end of file +} diff --git a/src/test/java/utils/EmailServiceHelperTest.java b/src/test/java/utils/EmailServiceHelperTest.java deleted file mode 100644 index fd3d543f..00000000 --- a/src/test/java/utils/EmailServiceHelperTest.java +++ /dev/null @@ -1,306 +0,0 @@ -package utils; - -import com.SleepUp.SU.accommodation.entity.Accommodation; -import com.SleepUp.SU.config.AppProperties; -import com.SleepUp.SU.reservation.entity.Reservation; -import com.SleepUp.SU.user.dto.UserRequest; -import com.SleepUp.SU.user.entity.User; -import com.SleepUp.SU.utils.email.EmailService; -import com.SleepUp.SU.utils.email.EmailServiceHelper; -import jakarta.mail.MessagingException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.time.LocalDate; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class EmailServiceHelperTest { - - @Mock - private EmailService emailService; - - @Mock - private AppProperties appProperties; - - @Mock - private AppProperties.MailProperties mailProperties; - - @InjectMocks - private EmailServiceHelper emailServiceHelper; - - private User guest; - private User owner; - private Accommodation accommodation; - private Reservation reservation; - private UserRequest userRequest; - - @BeforeEach - void setUp() { - guest = new User(); - guest.setEmail("guest@test.com"); - guest.setName("John Doe"); - - owner = new User(); - owner.setEmail("owner@test.com"); - owner.setName("Jane Smith"); - - accommodation = new Accommodation(); - accommodation.setName("Cozy Apartment"); - accommodation.setLocation("Madrid, Spain"); - accommodation.setManagedBy(owner); - - reservation = new Reservation(); - reservation.setCheckInDate(LocalDate.of(2025, 10, 1)); - reservation.setCheckOutDate(LocalDate.of(2025, 10, 5)); - - userRequest = new UserRequest("test@test.com", "testuser", "password123", "Test User"); - } - - @Test - void validateEmailConfig_WithValidConfig_EnablesEmail() { - when(appProperties.getMail()).thenReturn(mailProperties); - when(mailProperties.getFrom()).thenReturn("noreply@sleepup.com"); - when(mailProperties.getUsername()).thenReturn("username"); - when(mailProperties.getPassword()).thenReturn("password"); - - emailServiceHelper.validateEmailConfig(); - - verify(appProperties).getMail(); - } - - @Test - void validateEmailConfig_WithNullConfig_DisablesEmail() { - when(appProperties.getMail()).thenReturn(null); - - emailServiceHelper.validateEmailConfig(); - - verify(appProperties).getMail(); - } - - @Test - void sendWelcomeEmail_Success() throws MessagingException { - doNothing().when(emailService).sendWelcomeEmail(anyString(), anyString()); - - emailServiceHelper.sendWelcomeEmail(userRequest, guest); - - verify(emailService).sendWelcomeEmail(userRequest.email(), userRequest.username()); - } - - @Test - void sendWelcomeEmail_HandlesException() throws MessagingException { - doThrow(new MessagingException("Error")).when(emailService) - .sendWelcomeEmail(anyString(), anyString()); - - assertDoesNotThrow(() -> - emailServiceHelper.sendWelcomeEmail(userRequest, guest) - ); - - verify(emailService).sendWelcomeEmail(anyString(), anyString()); - } - - @Test - void sendOwnerReservedNotification_Success() throws MessagingException { - doNothing().when(emailService).sendOwnerReservedNotification(any(), any(), any()); - - emailServiceHelper.sendOwnerReservedNotification(guest, accommodation, reservation); - - verify(emailService).sendOwnerReservedNotification(guest, accommodation, reservation); - } - - @Test - void sendReservationConfirmationEmail_Success() throws MessagingException { - doNothing().when(emailService).sendReservationConfirmationEmail(any(), any(), any()); - - emailServiceHelper.sendReservationConfirmationEmail(guest, accommodation, reservation); - - verify(emailService).sendReservationConfirmationEmail(guest, accommodation, reservation); - } - - @Test - void sendReservationReminderEmail_Success() throws MessagingException { - doNothing().when(emailService).sendReservationReminderEmail(any(), any(), any()); - - emailServiceHelper.sendReservationReminderEmail(guest, accommodation, reservation); - - verify(emailService).sendReservationReminderEmail(guest, accommodation, reservation); - } - - @Test - void sendOwnerReservationReminderEmail_Success() throws MessagingException { - doNothing().when(emailService).sendOwnerReservationReminderEmail(any(), any()); - - emailServiceHelper.sendOwnerReservationReminderEmail(accommodation, reservation); - - verify(emailService).sendOwnerReservationReminderEmail(accommodation, reservation); - } - - @Test - void sendCancellationConfirmationEmail_Success() throws MessagingException { - doNothing().when(emailService).sendCancellationConfirmationEmail(any(), any(), any()); - - emailServiceHelper.sendCancellationConfirmationEmail(guest, accommodation, reservation); - - verify(emailService).sendCancellationConfirmationEmail(guest, accommodation, reservation); - } - - @Test - void sendCancellationByOwnerNotificationEmail_Success() throws MessagingException { - doNothing().when(emailService).sendCancellationByOwnerNotificationEmail(any(), any(), any()); - - emailServiceHelper.sendCancellationByOwnerNotificationEmail(guest, accommodation, reservation); - - verify(emailService).sendCancellationByOwnerNotificationEmail(guest, accommodation, reservation); - } - - @Test - void sendCancellationNotificationToOwnerEmail_Success() throws MessagingException { - doNothing().when(emailService).sendCancellationNotificationToOwnerEmail(any(), any(), any()); - - emailServiceHelper.sendCancellationNotificationToOwnerEmail(guest, accommodation, reservation); - - verify(emailService).sendCancellationNotificationToOwnerEmail(guest, accommodation, reservation); - } - - @Test - void handleNewReservationEmails_SendsBothEmails() throws MessagingException { - doNothing().when(emailService).sendReservationConfirmationEmail(any(), any(), any()); - doNothing().when(emailService).sendOwnerReservedNotification(any(), any(), any()); - - emailServiceHelper.handleNewReservationEmails(guest, accommodation, reservation); - - verify(emailService).sendReservationConfirmationEmail(guest, accommodation, reservation); - verify(emailService).sendOwnerReservedNotification(guest, accommodation, reservation); - } - - @Test - void handleGuestCancellationEmails_SendsBothEmails() throws MessagingException { - doNothing().when(emailService).sendCancellationConfirmationEmail(any(), any(), any()); - doNothing().when(emailService).sendCancellationNotificationToOwnerEmail(any(), any(), any()); - - emailServiceHelper.handleGuestCancellationEmails(guest, accommodation, reservation); - - verify(emailService).sendCancellationConfirmationEmail(guest, accommodation, reservation); - verify(emailService).sendCancellationNotificationToOwnerEmail(guest, accommodation, reservation); - } - - @Test - void handleOwnerCancellationEmails_SendsNotification() throws MessagingException { - doNothing().when(emailService).sendCancellationByOwnerNotificationEmail(any(), any(), any()); - - emailServiceHelper.handleOwnerCancellationEmails(guest, accommodation, reservation); - - verify(emailService).sendCancellationByOwnerNotificationEmail(guest, accommodation, reservation); - } - - @Test - void sendReservationReminders_SendsBothReminders() throws MessagingException { - doNothing().when(emailService).sendReservationReminderEmail(any(), any(), any()); - doNothing().when(emailService).sendOwnerReservationReminderEmail(any(), any()); - - emailServiceHelper.sendReservationReminders(guest, accommodation, reservation); - - verify(emailService).sendReservationReminderEmail(guest, accommodation, reservation); - verify(emailService).sendOwnerReservationReminderEmail(accommodation, reservation); - } - - @Test - void canSendEmails_WithValidUser_ReturnsTrue() { - boolean result = emailServiceHelper.canSendEmails(guest); - - assertTrue(result); - } - - @Test - void canSendEmails_WithNullUser_ReturnsFalse() { - boolean result = emailServiceHelper.canSendEmails(null); - - assertFalse(result); - } - - @Test - void canSendEmails_WithNullEmail_ReturnsFalse() { - guest.setEmail(null); - - boolean result = emailServiceHelper.canSendEmails(guest); - - assertFalse(result); - } - - @Test - void canSendEmails_WithEmptyEmail_ReturnsFalse() { - guest.setEmail(" "); - - boolean result = emailServiceHelper.canSendEmails(guest); - - assertFalse(result); - } - - @Test - void canSendReservationEmails_WithValidData_ReturnsTrue() { - boolean result = emailServiceHelper.canSendReservationEmails(guest, accommodation, reservation); - - assertTrue(result); - } - - @Test - void canSendReservationEmails_WithInvalidGuest_ReturnsFalse() { - guest.setEmail(null); - - boolean result = emailServiceHelper.canSendReservationEmails(guest, accommodation, reservation); - - assertFalse(result); - } - - @Test - void canSendReservationEmails_WithNullAccommodation_ReturnsFalse() { - boolean result = emailServiceHelper.canSendReservationEmails(guest, null, reservation); - - assertFalse(result); - } - - @Test - void canSendReservationEmails_WithNullOwner_ReturnsFalse() { - accommodation.setManagedBy(null); - - boolean result = emailServiceHelper.canSendReservationEmails(guest, accommodation, reservation); - - assertFalse(result); - } - - @Test - void canSendReservationEmails_WithNullReservation_ReturnsFalse() { - boolean result = emailServiceHelper.canSendReservationEmails(guest, accommodation, null); - - assertFalse(result); - } - - @Test - void canSendReservationEmails_WithInvalidOwnerEmail_ReturnsFalse() { - owner.setEmail(""); - - boolean result = emailServiceHelper.canSendReservationEmails(guest, accommodation, reservation); - - assertFalse(result); - } - - @Test - void allEmailMethods_HandleExceptionsGracefully() throws MessagingException { - doThrow(new MessagingException("Error")).when(emailService) - .sendReservationConfirmationEmail(any(), any(), any()); - doThrow(new MessagingException("Error")).when(emailService) - .sendOwnerReservedNotification(any(), any(), any()); - - assertDoesNotThrow(() -> - emailServiceHelper.handleNewReservationEmails(guest, accommodation, reservation) - ); - } -} \ No newline at end of file diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index 8b3bfecb..49ceb72c 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -36,6 +36,7 @@ spring.mail.properties.mail.smtp.ssl.trust=smtp.gmail.com spring.mail.properties.mail.smtp.timeout=5000 spring.mail.properties.mail.debug=false spring.mail.test-connection=false +management.health.mail.enabled=false # Optional: import external config file spring.config.import=optional:file:.env[.properties] \ No newline at end of file diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index cf1ef234..391ef0ad 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -5,11 +5,11 @@ SET FOREIGN_KEY_CHECKS = 0; INSERT INTO users (id, username, name, email, password, role) VALUES (99, 'TestUser', 'nameTest', 'usertnest@test.com', 'password123', 'USER'), (6,'disabled_user', 'Disabled User', 'disabledUser@sleepup.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER'), - (1, 'User1', 'Name1', 'user1@example.com', 'password1', 'USER'), -- owner of accommodations 1 and reservation 5 - (2, 'User2', 'Name2', 'user2@example.com', 'password2', 'USER'), -- owner of accommodations 2,5 and reservation 4,2 - (3, 'User3', 'Name3', 'user3@example.com', 'password3', 'USER'), -- owner of accommodation 3 and reservation 2 - (4, 'User4', 'Name4', 'user4@example.com', 'password4', 'USER'), -- owner of accommodation 4 and reservation 1 - (5, 'Admin1', 'Name5', 'admin@example.com', 'password5', 'ADMIN'); + (1, 'User1', 'Name1', 'user1@example.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER'), -- owner of accommodations 1 and reservation 5 + (2, 'User2', 'Name2', 'user2@example.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER'), -- owner of accommodations 2,5 and reservation 4,2 + (3, 'User3', 'Name3', 'user3@example.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER'), -- owner of accommodation 3 and reservation 2 + (4, 'User4', 'Name4', 'user4@example.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'USER'), -- owner of accommodation 4 and reservation 1 + (5, 'Admin1', 'Name5', 'admin@example.com', '$2a$10$HsMF2wIVlZAelTWGNHD/r.lbHJemKWx0.HEfqHKHF91CR8R3fDjX2', 'ADMIN'); -- Insert 5 accommodations managed by users INSERT INTO accommodations ( @@ -23,13 +23,13 @@ INSERT INTO accommodations ( -- Insert 5 reservations linked to users and accommodations INSERT INTO reservations ( - id, user_id, accommodation_id, check_in_date, check_out_date, booking_status, created_date, email_sent + id, user_id, accommodation_id, check_in_date, check_out_date, booking_status, created_date, email_sent, total_price ) VALUES - (1, 4, 1, '2025-09-21', '2025-09-24', 'CONFIRMED', NOW(), false), - (2, 2, 1, '2025-10-02', '2025-10-06', 'CONFIRMED', NOW(), false), - (3, 3, 3, '2025-09-16', '2025-09-20', 'CANCELLED', NOW(), false), - (4, 2, 4, '2025-10-06', '2025-10-12', 'CONFIRMED', NOW(), false), - (5, 1, 5, '2025-11-02', '2025-11-08', 'PENDING', NOW(), false); + (1, 4, 1, '2025-09-21', '2025-09-24', 'CONFIRMED', NOW(), false, 450.00), + (2, 2, 1, '2025-10-02', '2025-10-06', 'CONFIRMED', NOW(), false, 600.00), + (3, 3, 3, '2025-09-16', '2025-09-20', 'CANCELLED', NOW(), false, 0.00), + (4, 2, 4, '2025-10-06', '2025-10-12', 'CONFIRMED', NOW(), false, 750.00), + (5, 1, 5, '2025-11-02', '2025-11-08', 'PENDING', NOW(), false, 550.00); -- Reset sequences to avoid conflicts with future IDs ALTER TABLE users AUTO_INCREMENT = 5; diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql index 8d26a093..2f913bf7 100644 --- a/src/test/resources/schema.sql +++ b/src/test/resources/schema.sql @@ -44,6 +44,7 @@ CREATE TABLE reservations ( guest_number INT DEFAULT NULL, accommodation_id BIGINT NOT NULL, user_id BIGINT NOT NULL, + total_price DECIMAL(19,2) NOT NULL, KEY FK_accommodation_id (accommodation_id), KEY FK_user_id (user_id), CONSTRAINT FK_accommodation FOREIGN KEY (accommodation_id) REFERENCES accommodations(id),