diff --git a/CHANGELOG.md b/CHANGELOG.md index cb5f22db2..ef257ce71 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ ## CHANGELOG The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +* Issue #852: Added GET endpoint in the Challenge Microservice to retrieve resources associated to a challenge +* Issue #849: Added GET endpoint to retrieve all User's bookmarked challenges. +* Issue #843: Added DELETE endpoint in the Challenge Microservice to unbookmark a challenge. * Issue #843: Added DELETE endpoint in the Challenge Microservice to unbookmark a challenge. +* Issue #852: Added GET endpoint for retrieving resources from a challenge * Issue #829: Added bookmark POST endpoint in the Challenge Microservice to bookmark a challenge. * Issue #827: Added bookmark POST endpoint in the User Microservice to bookmark a challenge. * Issue #831: Added DELETE endpoint for user's bookmals in User Microservice. diff --git a/contributors.md b/contributors.md index fc25cea11..ad300ebf8 100755 --- a/contributors.md +++ b/contributors.md @@ -82,4 +82,10 @@ * Albert Marín Miranda - https://github.com/Almami679 * Alexandra Bonet - https://github.com/AlexandraBonetCanela * Gwénaël Le Moing - https://github.com/g-lemoing -* Matías Meza - https://github.com/RustyGearBox \ No newline at end of file +* Matías Meza - https://github.com/RustyGearBox +* Marc Bernabeu Rodriguez - https://github.com/trisk910 +* Toni Jiménez - https://github.com/tonijimenez72 +* Enric Vicente - https://github.com/EnricW +* Ismael Peiró - https://github.com/IsmaPeiro +* Santiago Hernandez Beltran - https://github.com/shernandez334 +* Inga Demetrashvili - https://github.com/IngaD89 \ No newline at end of file diff --git a/itachallenge-auth/src/main/java/com/itachallenge/auth/service/AuthService.java b/itachallenge-auth/src/main/java/com/itachallenge/auth/service/AuthService.java index 2a2f77694..fbe932443 100644 --- a/itachallenge-auth/src/main/java/com/itachallenge/auth/service/AuthService.java +++ b/itachallenge-auth/src/main/java/com/itachallenge/auth/service/AuthService.java @@ -5,7 +5,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; @@ -39,7 +38,6 @@ public class AuthService implements IAuthService { private final String clientSecret; - @Autowired public AuthService(WebClient.Builder webClientBuilder, @Value("${spring.security.oauth2.client.provider.github.token-uri}") String githubTokenUri, @Value("${spring.security.oauth2.client.provider.github.user-info-uri}") String githubUserInfoUri, diff --git a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/controller/ChallengeSolvedController.java b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/controller/ChallengeSolvedController.java new file mode 100644 index 000000000..0403041ee --- /dev/null +++ b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/controller/ChallengeSolvedController.java @@ -0,0 +1,53 @@ +package com.itachallenge.challenge.controller; + +import com.itachallenge.challenge.dto.SolvedDto; +import com.itachallenge.challenge.exception.BadRequestException; +import com.itachallenge.challenge.exception.JwtException; +import com.itachallenge.challenge.service.IChallengeService; +import com.itachallenge.challenge.service.IJwtService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import lombok.RequiredArgsConstructor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Mono; + +@RestController +@Validated +@RequestMapping(value = "/itachallenge/api/v1/challenge") +@RequiredArgsConstructor +public class ChallengeSolvedController { + + private static final Logger log = LoggerFactory.getLogger(ChallengeSolvedController.class); + + private IChallengeService challengeService; + private IJwtService jwtService; + + @PostMapping("/challenges/{challengeId}/solved") + @Operation( + operationId = "Add a challenge to User's solved challenges.", + summary = "Add a challenge to solved challenges.", + description = "The ID Challenge sent through the URI is added to the user's solved challenges. User Id is determined from the headers.", + responses = { + @ApiResponse(responseCode = "200", content = {@Content(schema = @Schema(implementation = SolvedDto.class), mediaType = "application/json")}), + @ApiResponse(responseCode = "400", description = "Missing or invalid authorization header."), + @ApiResponse(responseCode = "404", description = "The Challenge with given Id was not found."), + @ApiResponse(responseCode = "500", description = "Internal Server Error") + } + ) + public Mono> addChallengeToSolved( + @PathVariable String challengeId, + @RequestHeader(name = "Authorization", required = false) String authHeader) { + return Mono.fromCallable(() -> jwtService.getUserUuIdFromAuthenticationHeader(authHeader)) + .onErrorMap(JwtException.class, e -> new BadRequestException(e.getMessage())) + .flatMap(userId -> challengeService.addChallengeToSolved(challengeId, userId)) + .doOnError(error -> log.error("Error adding challenge to solved: {}", error.getMessage())) + .map(ResponseEntity::ok); + } +} diff --git a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/controller/ResourceController.java b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/controller/ResourceController.java index 0ce54e738..0d61860de 100644 --- a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/controller/ResourceController.java +++ b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/controller/ResourceController.java @@ -1,18 +1,28 @@ package com.itachallenge.challenge.controller; import com.itachallenge.challenge.dto.ResourceDto; + import com.itachallenge.challenge.service.IResourceService; import io.swagger.v3.oas.annotations.Operation; + +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; + +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.UUID; + @RestController @Validated @RequestMapping(value = "/itachallenge/api/v1/resource") @@ -41,5 +51,18 @@ public Mono> createNewResource(@RequestBody @Valid R return resourceService.createResource(resourceDto) .map(createdResource -> ResponseEntity.ok().body(createdResource)); } + + + @GetMapping("/challenge/{challengeId}") + @Operation(summary = "Get resources by challenge ID") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Resources found"), + @ApiResponse(responseCode = "400", description = "Invalid challenge ID"), + @ApiResponse(responseCode = "404", description = "No resources found"), + @ApiResponse(responseCode = "500", description = "Internal Server Error") + }) + public Flux getResourcesByChallengeId(@PathVariable UUID challengeId) { + return resourceService.getResourcesByChallengeId(challengeId); + } } diff --git a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/document/ChallengeDocument.java b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/document/ChallengeDocument.java index 34645ea65..e44c0e419 100755 --- a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/document/ChallengeDocument.java +++ b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/document/ChallengeDocument.java @@ -48,6 +48,9 @@ public class ChallengeDocument { @Field(name="times_bookmark") private Integer timesBookmark; + @Field(name="times_solved") + private Integer timesSolved; + @Field(name = "tags") private List tags; @@ -80,5 +83,10 @@ public void increaseTimesBookmark() { public void decreaseTimesBookmark() { timesBookmark =Integer.max(timesBookmark == null ? 0 : timesBookmark - 1, 0); } + + public void increaseTimesSolved() { + timesSolved = timesSolved == null ? 1 : timesSolved + 1; + } + } diff --git a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/dto/ChallengeDto.java b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/dto/ChallengeDto.java index 139ac07f7..41ff16c9d 100755 --- a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/dto/ChallengeDto.java +++ b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/dto/ChallengeDto.java @@ -63,4 +63,8 @@ public class ChallengeDto { @JsonProperty(index = 11) private Integer timesBookmark; + + @JsonProperty(index = 12) + private Integer timesSolved; + } diff --git a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/dto/SolvedDto.java b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/dto/SolvedDto.java new file mode 100644 index 000000000..7c46a0421 --- /dev/null +++ b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/dto/SolvedDto.java @@ -0,0 +1,14 @@ +package com.itachallenge.challenge.dto; + +import lombok.*; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class SolvedDto { + + private boolean isSolved; + + private int timesSolved; + +} diff --git a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/enums/UserChallengeActionType.java b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/enums/UserChallengeActionType.java index 31269c178..b53a8f0b6 100644 --- a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/enums/UserChallengeActionType.java +++ b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/enums/UserChallengeActionType.java @@ -2,5 +2,6 @@ public enum UserChallengeActionType { BOOKMARKS, - FAVORITES + FAVORITES, + SOLVED } diff --git a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/exception/GlobalExceptionHandler.java b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/exception/GlobalExceptionHandler.java index f255484f6..3d55c4c65 100755 --- a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/exception/GlobalExceptionHandler.java +++ b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/exception/GlobalExceptionHandler.java @@ -62,7 +62,7 @@ public ResponseEntity handleChallengeNotFoundReturn404Exception(Chal @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity handleResourceNotFoundException(ResourceNotFoundException ex) { - return ResponseEntity.ok().body(new MessageDto(ex.getMessage())); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new MessageDto(ex.getMessage())); } @ExceptionHandler(LanguageNotFoundException.class) diff --git a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/helper/DocumentToDtoConverter.java b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/helper/DocumentToDtoConverter.java index 876f99785..83125e83e 100755 --- a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/helper/DocumentToDtoConverter.java +++ b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/helper/DocumentToDtoConverter.java @@ -54,7 +54,8 @@ protected String convert(LocalDateTime creationDateFromDocument) { .addMapping(ChallengeDocument::getTitle, ChallengeDto::setTitle) .addMapping(ChallengeDocument::getTimesFavorite, ChallengeDto::setTimesFavorite) .addMapping(ChallengeDocument::getTags, ChallengeDto::setTags) - .addMapping(ChallengeDocument::getTimesBookmark, ChallengeDto::setTimesBookmark); + .addMapping(ChallengeDocument::getTimesBookmark, ChallengeDto::setTimesBookmark) + .addMapping(ChallengeDocument::getTimesSolved, ChallengeDto::setTimesSolved); mapper.addConverter(converterFromLocalDateTimeToString); } diff --git a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/repository/ResourceRepository.java b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/repository/ResourceRepository.java index e989cb36f..18dfee3f8 100644 --- a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/repository/ResourceRepository.java +++ b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/repository/ResourceRepository.java @@ -25,4 +25,5 @@ public interface ResourceRepository extends ReactiveSortingRepository saveAll(Flux resourceDocumentFlux); Mono deleteAll(); + Flux findByChallengeIdsContaining(UUID challengeId); } diff --git a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/ChallengeServiceImpl.java b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/ChallengeServiceImpl.java index 3acb8de3a..1ae8b1cb7 100644 --- a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/ChallengeServiceImpl.java +++ b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/ChallengeServiceImpl.java @@ -448,5 +448,33 @@ public Mono removeChallengeFromBookmarks(String challengeId, String }); } +@Override +public Mono addChallengeToSolved(String challengeId, String userId) { + Mono challengeIdMono = validateUUID(challengeId); + Mono userIdMono = validateUUID(userId); + + return Mono.zip(challengeIdMono, userIdMono) + .flatMap(uuidTuple -> { + UUID challengeUuid = uuidTuple.getT1(); + UUID userUuid = uuidTuple.getT2(); + + return challengeRepository.findByUuid(challengeUuid) + .switchIfEmpty(Mono.error(new ChallengeNotFoundReturn404Exception( + String.format(CHALLENGE_NOT_FOUND_ERROR, challengeUuid)))) + .flatMap(challenge -> { + log.info("It should be connected to user service using the fields {} and {}", challengeUuid, userUuid); + + //The part of the user service is not implemented yet, it should be connected to the user service in the future. + + if (Optional.ofNullable(challenge.getTimesSolved()).orElse(0) == 0) { + challenge.increaseTimesSolved(); + return challengeRepository.save(challenge); + } + + return Mono.just(challenge); + }) + .map(updatedChallenge -> new SolvedDto(true, updatedChallenge.getTimesSolved())); + }); +} } \ No newline at end of file diff --git a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/IChallengeService.java b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/IChallengeService.java index 89965150f..fbafb906f 100755 --- a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/IChallengeService.java +++ b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/IChallengeService.java @@ -42,4 +42,6 @@ Flux> getChallengesByFilter(Optional idLa Mono removeChallengeFromFavorites(String challengeId, String userId); Mono removeChallengeFromBookmarks(String challengeId, String userId); + + Mono addChallengeToSolved(String challengeId, String userId); } diff --git a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/IResourceService.java b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/IResourceService.java index 831dba342..9f4169d72 100644 --- a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/IResourceService.java +++ b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/IResourceService.java @@ -1,12 +1,15 @@ package com.itachallenge.challenge.service; import com.itachallenge.challenge.dto.*; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.UUID; public interface IResourceService { Mono createResource(ResourceDto resourceDto); + Flux getResourcesByChallengeId(UUID challengeId); } diff --git a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/IUserService.java b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/IUserService.java index 8e22cd1e3..240204577 100644 --- a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/IUserService.java +++ b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/IUserService.java @@ -9,4 +9,6 @@ public interface IUserService { Mono removeChallengeFromFavorites(String userId, String challengeId); Mono removeChallengeFromBookmarks(String userId, String challengeId); + + Mono addChallengeToSolved(String userId, String challengeId); } diff --git a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/ResourceServiceImpl.java b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/ResourceServiceImpl.java index e3b5fd5de..55b4bd81b 100644 --- a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/ResourceServiceImpl.java +++ b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/ResourceServiceImpl.java @@ -5,12 +5,16 @@ import com.itachallenge.challenge.dto.ResourceDto; import com.itachallenge.challenge.enums.AssociationType; import com.itachallenge.challenge.enums.Topic; +import com.itachallenge.challenge.exception.BadRequestException; +import com.itachallenge.challenge.exception.InternalServerErrorException; +import com.itachallenge.challenge.exception.ResourceNotFoundException; import com.itachallenge.challenge.helper.DocumentToDtoConverter; import com.itachallenge.challenge.repository.ResourceRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.CacheEvict; import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.*; @@ -135,4 +139,22 @@ private Mono saveResource(ResourceDto resourceDto) { }) .doOnError(error -> log.error("Error occurred when creating resource {}", error.getMessage())); } + + + public Flux getResourcesByChallengeId(UUID challengeId) { + return Mono.justOrEmpty(challengeId) + .switchIfEmpty(Mono.error(new BadRequestException("Challenge ID cannot be null"))) + .flatMapMany(validChallengeId -> + resourceRepository.findByChallengeIdsContaining(validChallengeId) + .switchIfEmpty(Mono.error(new ResourceNotFoundException("No resources found for challenge ID: " + validChallengeId))) // Lanzar 404 si no se encuentra nada + .map(resourceDoc -> resourceConverter.convertDocumentToDto(resourceDoc, ResourceDto.class)) + .onErrorResume(error -> { + log.error("Error fetching resources for challenge ID {}: {}", validChallengeId, error.getMessage()); + if (error instanceof ResourceNotFoundException) { + return Flux.error(error); + } + return Flux.error(new InternalServerErrorException("Failed to fetch resources for challenge ID: " + validChallengeId)); + }) + ); + } } \ No newline at end of file diff --git a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/UserServiceImpl.java b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/UserServiceImpl.java index 7c8f24e7b..69299a79f 100644 --- a/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/UserServiceImpl.java +++ b/itachallenge-challenge/src/main/java/com/itachallenge/challenge/service/UserServiceImpl.java @@ -24,6 +24,7 @@ public class UserServiceImpl implements IUserService { private final String userServiceUrl; private final String X_FAVORITE_MESSAGE = "X-Favorite-Message"; private final String X_BOOKMARK_MESSAGE = "X-Bookmark-Message"; + private final String X_SOLVED_MESSAGE = "X-Solved-Message"; public UserServiceImpl( WebClient.Builder webClientBuilder, @@ -42,6 +43,11 @@ public Mono addChallengeToBookmarks(String userId, String challengeId) return callEndpoint(userId, challengeId, UserChallengeActionType.BOOKMARKS, X_BOOKMARK_MESSAGE, HttpMethod.POST); } + @Override + public Mono addChallengeToSolved(String userId, String challengeId) { + return callEndpoint(userId, challengeId, UserChallengeActionType.SOLVED, X_SOLVED_MESSAGE, HttpMethod.POST); + } + @Override public Mono removeChallengeFromFavorites(String userId, String challengeId) { return callEndpoint(userId, challengeId, UserChallengeActionType.FAVORITES, X_FAVORITE_MESSAGE, HttpMethod.DELETE); diff --git a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/controller/ChallengeSolvedControllerTest.java b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/controller/ChallengeSolvedControllerTest.java new file mode 100644 index 000000000..c570d4a96 --- /dev/null +++ b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/controller/ChallengeSolvedControllerTest.java @@ -0,0 +1,97 @@ +package com.itachallenge.challenge.controller; + +import com.itachallenge.challenge.dto.SolvedDto; +import com.itachallenge.challenge.exception.BadRequestException; +import com.itachallenge.challenge.exception.JwtException; +import com.itachallenge.challenge.service.IChallengeService; +import com.itachallenge.challenge.service.IJwtService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.ResponseEntity; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + + +class ChallengeSolvedControllerTest { + + @Mock + private IChallengeService challengeService; + + @Mock + private IJwtService jwtService; + + @InjectMocks + private ChallengeSolvedController challengeSolvedController; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void addChallengeToSolved_Success() { + String challengeId = "123"; + String authHeader = "Bearer validToken"; + String userId = "user123"; + SolvedDto solvedDto = new SolvedDto(); + + when(jwtService.getUserUuIdFromAuthenticationHeader(authHeader)).thenReturn(userId); + when(challengeService.addChallengeToSolved(challengeId, userId)).thenReturn(Mono.just(solvedDto)); + + Mono> result = challengeSolvedController.addChallengeToSolved(challengeId, authHeader); + + StepVerifier.create(result) + .assertNext(response -> { + assertEquals(200, response.getStatusCodeValue()); + assertEquals(solvedDto, response.getBody()); + }) + .verifyComplete(); + + verify(jwtService, times(1)).getUserUuIdFromAuthenticationHeader(authHeader); + verify(challengeService, times(1)).addChallengeToSolved(challengeId, userId); + } + + @Test + void addChallengeToSolved_InvalidAuthHeader() { + String challengeId = "123"; + String authHeader = "invalidToken"; + + when(jwtService.getUserUuIdFromAuthenticationHeader(authHeader)).thenThrow(new JwtException("Invalid token")); + + Mono> result = challengeSolvedController.addChallengeToSolved(challengeId, authHeader); + + StepVerifier.create(result) + .expectErrorMatches(throwable -> throwable instanceof BadRequestException && + throwable.getMessage().equals("Invalid token")) + .verify(); + + verify(jwtService, times(1)).getUserUuIdFromAuthenticationHeader(authHeader); + verifyNoInteractions(challengeService); + } + + @Test + void addChallengeToSolved_ServiceError() { + String challengeId = "123"; + String authHeader = "Bearer validToken"; + String userId = "user123"; + + when(jwtService.getUserUuIdFromAuthenticationHeader(authHeader)).thenReturn(userId); + when(challengeService.addChallengeToSolved(challengeId, userId)).thenReturn(Mono.error(new RuntimeException("Service error"))); + + Mono> result = challengeSolvedController.addChallengeToSolved(challengeId, authHeader); + + StepVerifier.create(result) + .expectErrorMatches(throwable -> throwable instanceof RuntimeException && + throwable.getMessage().equals("Service error")) + .verify(); + + verify(jwtService, times(1)).getUserUuIdFromAuthenticationHeader(authHeader); + verify(challengeService, times(1)).addChallengeToSolved(challengeId, userId); + } +} \ No newline at end of file diff --git a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/controller/ResourceControllerTest.java b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/controller/ResourceControllerTest.java index eb50a4f21..dc520e216 100644 --- a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/controller/ResourceControllerTest.java +++ b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/controller/ResourceControllerTest.java @@ -1,9 +1,11 @@ package com.itachallenge.challenge.controller; + import com.itachallenge.challenge.dto.ResourceDto; import com.itachallenge.challenge.enums.AssociationType; import com.itachallenge.challenge.enums.ResourceContentType; import com.itachallenge.challenge.enums.Topic; +import com.itachallenge.challenge.exception.ResourceNotFoundException; import com.itachallenge.challenge.service.IResourceService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -13,6 +15,7 @@ import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.reactive.server.WebTestClient; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import static org.junit.Assert.*; @@ -23,6 +26,7 @@ import static org.mockito.ArgumentMatchers.any; +import java.lang.reflect.Constructor; import java.util.List; import java.util.UUID; @@ -107,5 +111,78 @@ void createNewResource_MissingRequiredFields_ReturnsBadRequest() { verify(resourceService, never()).createResource(any(ResourceDto.class)); } + @Test + void getResourcesByChallengeId_ValidId_ReturnsResources() { + + UUID challengeId = UUID.randomUUID(); + ResourceDto mockResource = ResourceDto.builder() + .resourceId(UUID.randomUUID()) + .title("Test Resource") + .description("Test Description") + .url("http://test.com") + .topic(Topic.COMPONENTS) + .contentType(ResourceContentType.VIDEO) + .challengeIds(List.of(challengeId)) + .associationType(AssociationType.ALLSAMETOPIC) + .build(); + + when(resourceService.getResourcesByChallengeId(challengeId)) + .thenReturn(Flux.just(mockResource)); + + + webTestClient.get() + .uri("/itachallenge/api/v1/resource/challenge/" + challengeId) // Ruta completa + .exchange() + .expectStatus().isOk() + .expectBodyList(ResourceDto.class) + .hasSize(1) + .contains(mockResource); + + verify(resourceService, times(1)).getResourcesByChallengeId(challengeId); + } + + @Test + void getResourcesByChallengeId_InvalidId_ReturnsBadRequest() { + + webTestClient.get() + .uri("/itachallenge/api/v1/resource/challenge/null") // Ruta completa con ID inválido + .exchange() + .expectStatus().isBadRequest(); + + + verify(resourceService, never()).getResourcesByChallengeId(any()); + } + + @Test + void getResourcesByChallengeId_NoResources_ReturnsNotFoundWithMessage() { + + UUID challengeId = UUID.randomUUID(); + String errorMessage = "No resources found for challenge ID: " + challengeId; + + when(resourceService.getResourcesByChallengeId(challengeId)) + .thenReturn(Flux.error(new ResourceNotFoundException(errorMessage))); + + + webTestClient.get() + .uri("/itachallenge/api/v1/resource/challenge/" + challengeId) + .exchange() + .expectStatus().isNotFound() + .expectBody() + .jsonPath("$.message").isEqualTo(errorMessage); + + verify(resourceService, times(1)).getResourcesByChallengeId(challengeId); + } + } + + + + + + + + + + + diff --git a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/document/ChallengeTest.java b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/document/ChallengeTest.java index 446615ae8..9bed12d69 100755 --- a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/document/ChallengeTest.java +++ b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/document/ChallengeTest.java @@ -28,6 +28,7 @@ void getUuid() { Topic.LISTS, null, null, + null, tags); assertEquals(uuid, challenge.getUuid()); } @@ -45,6 +46,7 @@ void getTitle() { Topic.COMPONENTS, null, null, + null, tags); assertEquals(expectedTitle, challenge.getTitle()); } @@ -62,6 +64,7 @@ void getLevel() { Topic.COMPONENTS, null, null, + null, tags); assertEquals(level, challenge.getLevel()); } @@ -79,6 +82,7 @@ void getCreationDate() { Topic.COMPONENTS, null, null, + null, tags); assertTrue(creationDate.truncatedTo(ChronoUnit.SECONDS).isEqual(challenge.getCreationDate().truncatedTo(ChronoUnit.SECONDS))); } @@ -96,6 +100,7 @@ void getDetail() { Topic.COMPONENTS, null, null, + null, tags); assertEquals(detail, challenge.getDetail()); } @@ -108,7 +113,7 @@ void getLanguages() { "https://res.cloudinary.com/itachallenge/image/upload/v1739361249/language_icon_Javascript_asgn04.svg"), new LanguageDocument(uuid2, "Python", "https://res.cloudinary.com/itachallenge/image/upload/v1739361249/language_icon_Python_rphody.svg")); - ChallengeDocument challenge = new ChallengeDocument(null, null, null, null, null, languages, null, Topic.COMPONENTS, null, null, tags); + ChallengeDocument challenge = new ChallengeDocument(null, null, null, null, null, languages, null, Topic.COMPONENTS, null, null,null, tags); assertEquals(languages, challenge.getLanguages()); } @@ -126,6 +131,7 @@ void getSolutions() { Topic.COMPONENTS, null, null, + null, tags); assertEquals(solutions, challenge.getSolutions()); } @@ -144,6 +150,7 @@ void getTimesFavorite() { Topic.COMPONENTS, timesFavorite, null, + null, tags); assertEquals(timesFavorite, challenge.getTimesFavorite()); } @@ -152,10 +159,29 @@ void getTimesFavorite() { void getTimesBookmark(){ int timesBookmark = 30; - ChallengeDocument challenge = new ChallengeDocument(null, null, null, null, null, null, null, Topic.COMPONENTS, null, timesBookmark, tags); + ChallengeDocument challenge = new ChallengeDocument(null, null, null, null, null, null, null, Topic.COMPONENTS, null, timesBookmark,null, tags); assertEquals(timesBookmark, challenge.getTimesBookmark()); } + @Test + void getTimesSolved() { + int timesSolved = 21; + + ChallengeDocument challenge = new ChallengeDocument(null, + null, + null, + null, + null, + null, + null, + Topic.COMPONENTS, + null, + null, + timesSolved, + tags); + assertEquals(timesSolved, challenge.getTimesSolved()); + } + @Test void getTagsTest() { UUID uuid = UUID.randomUUID(); @@ -172,6 +198,7 @@ void getTagsTest() { Topic.COMPONENTS, 20, null, + null, List.of(tag.getIdTag()) ); @@ -179,6 +206,30 @@ void getTagsTest() { assertEquals(1, challenge.getTags().size()); } + @Test + void increaseTimesSolved_whenTimesSolvedIsNull_shouldSetToOne() { + ChallengeDocument challenge = ChallengeDocument.builder() + .uuid(UUID.randomUUID()) + .timesSolved(null) + .build(); + + challenge.increaseTimesSolved(); + + assertEquals(1, challenge.getTimesSolved()); + } + + @Test + void increaseTimesSolved_whenTimesSolvedIsNonNull_shouldIncrementByOne() { + ChallengeDocument challenge = ChallengeDocument.builder() + .uuid(UUID.randomUUID()) + .timesSolved(3) + .build(); + + challenge.increaseTimesSolved(); + + assertEquals(4, challenge.getTimesSolved()); + } + @Test void setTagsTest() { @@ -190,6 +241,7 @@ void setTagsTest() { Topic.COMPONENTS, 20, null, + null, new ArrayList(List.of(firstTag.getIdTag())) { } ); diff --git a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/dto/SolvedDtoTest.java b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/dto/SolvedDtoTest.java new file mode 100644 index 000000000..dcdc73d52 --- /dev/null +++ b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/dto/SolvedDtoTest.java @@ -0,0 +1,46 @@ +package com.itachallenge.challenge.dto; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SolvedDtoTest { + + @Test + void testNoArgsConstructorAndSetters() { + SolvedDto dto = new SolvedDto(); + dto.setSolved(true); + dto.setTimesSolved(5); + + assertTrue(dto.isSolved()); + assertEquals(5, dto.getTimesSolved()); + } + + @Test + void testAllArgsConstructor() { + SolvedDto dto = new SolvedDto(false, 3); + + assertFalse(dto.isSolved()); + assertEquals(3, dto.getTimesSolved()); + } + + @Test + void testEqualsAndHashCode() { + SolvedDto dto1 = new SolvedDto(true, 2); + SolvedDto dto2 = new SolvedDto(true, 2); + SolvedDto dto3 = new SolvedDto(false, 5); + + assertEquals(dto1, dto2); + assertEquals(dto1.hashCode(), dto2.hashCode()); + assertNotEquals(dto1, dto3); + } + + @Test + void testToString() { + SolvedDto dto = new SolvedDto(true, 7); + String result = dto.toString(); + + assertTrue(result.contains("isSolved=true")); + assertTrue(result.contains("timesSolved=7")); + } +} diff --git a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/helper/ChallengeDocumentToDtoConverterTest.java b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/helper/ChallengeDocumentToDtoConverterTest.java index a187bd8cb..e6e1e87b8 100755 --- a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/helper/ChallengeDocumentToDtoConverterTest.java +++ b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/helper/ChallengeDocumentToDtoConverterTest.java @@ -59,12 +59,13 @@ public void setUp() { Topic topic = Topic.DEBUGGING; int timesFavorite = 20; int timesBookmark = 30; + int timesSolved = 40; challengeDoc1 = new ChallengeDocument(challengeRandomId1, title, level, localDateTime, detail, - Set.of(languageDoc1, languageDoc2), List.of(solutionsRandomId), topic, timesFavorite, timesBookmark, tags); + Set.of(languageDoc1, languageDoc2), List.of(solutionsRandomId), topic, timesFavorite, timesBookmark, timesSolved, tags); challengeDoc2 = new ChallengeDocument(challengeRandomId2, title, level, localDateTime, detail, - Set.of(languageDoc1, languageDoc2), List.of(solutionsRandomId), topic, timesFavorite, timesBookmark, tags); + Set.of(languageDoc1, languageDoc2), List.of(solutionsRandomId), topic, timesFavorite, timesBookmark, timesSolved, tags); challengeDto1 = getChallengeDtoMocked(challengeRandomId1, title, level, creationDate, detail, Set.of(languageDto1, languageDto2), @@ -125,6 +126,7 @@ private ChallengeDto getChallengeDtoMocked(UUID challengeId, String title, Strin when(challengeDocMocked.getTopic()).thenReturn(Topic.DEBUGGING); when(challengeDocMocked.getTimesFavorite()).thenReturn(20); when(challengeDocMocked.getTimesBookmark()).thenReturn(30); + when(challengeDocMocked.getTimesSolved()).thenReturn(40); when(challengeDocMocked.getTags()).thenReturn(tags); return challengeDocMocked; } diff --git a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/integration/ChallengeIntegrationTest.java b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/integration/ChallengeIntegrationTest.java index 234f8c008..2ce59d27a 100644 --- a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/integration/ChallengeIntegrationTest.java +++ b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/integration/ChallengeIntegrationTest.java @@ -83,10 +83,10 @@ public void setUp() { ChallengeDocument challenge = new ChallengeDocument (uuid_1, title1, "Level 1", LocalDateTime.now(), detail, languageSet, - solutionList, Topic.LISTS, 20, 5, tags); + solutionList, Topic.LISTS, 20, 5,2, tags); ChallengeDocument challenge2 = new ChallengeDocument (uuid_2, title2, "Level 2", LocalDateTime.now(), detail, languageSet, - solutionList, Topic.COMPONENTS, 20, 30, tags); + solutionList, Topic.COMPONENTS, 20, 30,2, tags); challengeRepository.saveAll(Flux.just(challenge, challenge2)).blockLast(); } diff --git a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/repository/ChallengeRepositoryTest.java b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/repository/ChallengeRepositoryTest.java index 9adcad577..b6be94c00 100755 --- a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/repository/ChallengeRepositoryTest.java +++ b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/repository/ChallengeRepositoryTest.java @@ -18,7 +18,6 @@ import java.time.Duration; import java.time.LocalDateTime; import java.util.*; -import java.util.Locale; import static org.junit.Assert.*; import static org.springframework.test.util.AssertionErrors.fail; @@ -66,6 +65,8 @@ public void setUp() { List favoritedByList = List.of(UUID.randomUUID(),UUID.randomUUID()); + List solvedByList = List.of(UUID.randomUUID(),UUID.randomUUID()); + DetailDocument detail = new DetailDocument(description); String title1 = "Loops"; @@ -74,13 +75,13 @@ public void setUp() { ChallengeDocument challenge = new ChallengeDocument (uuid_1, title1, "MEDIUM", LocalDateTime.now(), detail, languageSet, solutionList, - Topic.DEBUGGING, 5, 3, tags); + Topic.DEBUGGING, 5, 3, 4, tags); ChallengeDocument challenge2 = new ChallengeDocument (uuid_2, title2, "EASY", LocalDateTime.now(), detail, languageSet, solutionList, - Topic.LISTS, 10, 2, tags); + Topic.LISTS, 10, 2, 7, tags); ChallengeDocument challenge3 = new ChallengeDocument (uuid_3, title3, "HARD", LocalDateTime.now(), detail, languageSet3, solutionList, - Topic.COMPONENTS, 15, 1, tags); + Topic.COMPONENTS, 15, 1, 9, tags); challengeRepository.saveAll(Flux.just(challenge, challenge2, challenge3)).blockLast(); diff --git a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/repository/ResourceRepositoryTest.java b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/repository/ResourceRepositoryTest.java index 88a14a770..b64a54095 100644 --- a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/repository/ResourceRepositoryTest.java +++ b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/repository/ResourceRepositoryTest.java @@ -251,4 +251,47 @@ void deleteAllResourcesTest() { .expectNext(0L) .verifyComplete(); } + + + @Test + void findByChallengeIdsContaining_WhenChallengeIdExists_ReturnsResources() { + + UUID targetChallengeId = UUID.fromString("8ecbfe54-fec8-11ed-be56-0242ac120002"); + + + ResourceDocument resourceWithTargetChallenge = ResourceDocument.builder() + .resourceId(uuid1) + .title("Title1") + .description("Description1") + .url("http://example.com/resource1") + .topic(Topic.DEBUGGING) + .contentType(ResourceContentType.BLOG) + .challengeIds(List.of(targetChallengeId)) + .build(); + + ResourceDocument resourceWithoutTargetChallenge = ResourceDocument.builder() + .resourceId(uuid2) + .title("Title2") + .description("Description2") + .url("http://example.com/resource2") + .topic(Topic.DEBUGGING) + .contentType(ResourceContentType.BLOG) + .challengeIds(List.of(UUID.randomUUID())) + .build(); + + + resourceRepository.deleteAll().block(); + resourceRepository.saveAll(Flux.just(resourceWithTargetChallenge, resourceWithoutTargetChallenge)).blockLast(); + + + Flux result = resourceRepository.findByChallengeIdsContaining(targetChallengeId); + + + StepVerifier.create(result) + .expectNextMatches(resource -> + resource.getChallengeIds().contains(targetChallengeId) && + resource.getResourceId().equals(uuid1) + ) + .verifyComplete(); + } } diff --git a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/service/ChallengeServiceImplTest.java b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/service/ChallengeServiceImplTest.java index 7c2835d55..18f981acc 100644 --- a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/service/ChallengeServiceImplTest.java +++ b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/service/ChallengeServiceImplTest.java @@ -30,6 +30,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import static org.assertj.core.api.Assertions.assertThat; @@ -104,7 +105,7 @@ void setUp() { challengeDocument = new ChallengeDocument(challengeRandomId, title, level, localDateTime, detail, Set.of(languageDocument), List.of(solutionsRandomId), Topic.COMPONENTS, - 20, 30,tags); + 20, 30, 40, tags); challengeDto = getChallengeDtoMocked(challengeRandomId, title, level, creationDate, detail, Set.of(languageDto), @@ -315,6 +316,78 @@ void testGetSolutions_ChallengeNotFound() { verify(solutionConverter, never()).convertDocumentFluxToDtoFlux(any(), any()); } +@Test +void addChallengeToSolved_WhenChallengeTimesSolvedIsNull_IncreasesTimesSolvedAndReturnsSolvedDTO() { + UUID challengeUuid = UUID.randomUUID(); + UUID userUuid = UUID.randomUUID(); + ChallengeDocument challenge = new ChallengeDocument(); + + challenge.setTimesSolved(null); + + when(challengeRepository.findByUuid(challengeUuid)).thenReturn(Mono.just(challenge)); + when(challengeRepository.save(challenge)).thenReturn(Mono.just(challenge)); + + StepVerifier.create(challengeService.addChallengeToSolved(challengeUuid.toString(), userUuid.toString())) + .expectNextMatches(solvedDto -> { + return solvedDto.getTimesSolved() == 1 && + solvedDto.isSolved(); + }) + .verifyComplete(); + + Assertions.assertEquals(1, challenge.getTimesSolved()); + + verify(challengeRepository, times(1)).findByUuid(challengeUuid); + verify(challengeRepository, times(1)).save(challenge); +} + +@Test +void addChallengeToSolved_WhenChallengeTimesSolvedIsZero_IncreasesTimesSolvedAndReturnsSolvedDTO() { + UUID challengeUuid = UUID.randomUUID(); + UUID userUuid = UUID.randomUUID(); + ChallengeDocument challenge = new ChallengeDocument(); + + challenge.setTimesSolved(0); + + when(challengeRepository.findByUuid(challengeUuid)).thenReturn(Mono.just(challenge)); + when(challengeRepository.save(challenge)).thenReturn(Mono.just(challenge)); + + StepVerifier.create(challengeService.addChallengeToSolved(challengeUuid.toString(), userUuid.toString())) + .expectNextMatches(solvedDto -> { + return solvedDto.getTimesSolved() == 1 && + solvedDto.isSolved(); + }) + .verifyComplete(); + + Assertions.assertEquals(1, challenge.getTimesSolved()); + + verify(challengeRepository, times(1)).findByUuid(challengeUuid); + verify(challengeRepository, times(1)).save(challenge); +} + +@Test +void addChallengeToSolved_WhenChallengeAlreadySolved_DoesNotIncreaseTimesSolvedAndReturnsSolvedDTO() { + UUID challengeUuid = UUID.randomUUID(); + UUID userUuid = UUID.randomUUID(); + ChallengeDocument challenge = new ChallengeDocument(); + int initialTimesSolved = 5; + + challenge.setTimesSolved(initialTimesSolved); + + when(challengeRepository.findByUuid(challengeUuid)).thenReturn(Mono.just(challenge)); + + StepVerifier.create(challengeService.addChallengeToSolved(challengeUuid.toString(), userUuid.toString())) + .expectNextMatches(solvedDto -> { + return solvedDto.getTimesSolved() == initialTimesSolved && + solvedDto.isSolved(); + }) + .verifyComplete(); + + Assertions.assertEquals(initialTimesSolved, challenge.getTimesSolved()); + + verify(challengeRepository, times(1)).findByUuid(challengeUuid); + verify(challengeRepository, times(0)).save(any()); +} + @Test void addSolution_ValidChallengeIdAndLanguageId_SolutionAdded() { // Arrange @@ -551,6 +624,15 @@ void addChallengeToBookmarks_WhenChallengeUuidNotValid_ReturnsError() { .verify(); } + @Test + void addChallengeToSolved_WhenChallengeUuidNotValid_ReturnsError() { + StepVerifier.create(challengeService.addChallengeToSolved("InvalidUuid", UUID.randomUUID().toString())) + .expectErrorMatches(error -> + error instanceof BadUUIDException && + error.getMessage().equals("Invalid ID format. Please indicate the correct format.")) + .verify(); + } + @Test void addChallengeToFavorites_WhenUserUuidNotValid_ReturnsError() { StepVerifier.create(challengeService.addChallengeToFavorites(UUID.randomUUID().toString(), "InvalidUuid")) @@ -569,6 +651,15 @@ void addChallengeToBookmarks_WhenUserUuidNotValid_ReturnsError() { .verify(); } + @Test + void addChallengeToSolved_WhenUserUuidNotValid_ReturnsError() { + StepVerifier.create(challengeService.addChallengeToSolved(UUID.randomUUID().toString(), "InvalidUuid")) + .expectErrorMatches(error -> + error instanceof BadUUIDException && + error.getMessage().equals("Invalid ID format. Please indicate the correct format.")) + .verify(); + } + @Test void addChallengeToFavorites_WhenChallengeUuidIsNull_ReturnsError() { StepVerifier.create(challengeService.addChallengeToFavorites(null, UUID.randomUUID().toString())) @@ -587,6 +678,15 @@ void addChallengeToBookmarks_WhenChallengeUuidIsNull_ReturnsError() { .verify(); } + @Test + void addChallengeToSolved_WhenChallengeUuidIsNull_ReturnsError() { + StepVerifier.create(challengeService.addChallengeToSolved(null, UUID.randomUUID().toString())) + .expectErrorMatches(error -> + error instanceof BadUUIDException && + error.getMessage().equals("Invalid ID format. Please indicate the correct format.")) + .verify(); + } + @Test void addChallengeToFavorites_WhenUserUuidIsNull_ReturnsError() { StepVerifier.create(challengeService.addChallengeToFavorites(UUID.randomUUID().toString(), null)) @@ -605,6 +705,15 @@ void addChallengeToBookmarks_WhenUserUuidIsNull_ReturnsError() { .verify(); } + @Test + void addChallengeToSolved_WhenUserUuidIsNull_ReturnsError() { + StepVerifier.create(challengeService.addChallengeToSolved(UUID.randomUUID().toString(), null)) + .expectErrorMatches(error -> + error instanceof BadUUIDException && + error.getMessage().equals("Invalid ID format. Please indicate the correct format.")) + .verify(); + } + @Test void addChallengeToFavorites_WhenChallengeNotFound_ReturnsError() { String CHALLENGE_NOT_FOUND_ERROR = "Challenge with id: %s not found"; @@ -637,6 +746,22 @@ void addChallengeToBookmarks_WhenChallengeNotFound_ReturnsError() { verify(challengeRepository, times(1)).findByUuid(challengeUuid); } + @Test + void addChallengeToSolved_WhenChallengeNotFound_ReturnsError() { + String CHALLENGE_NOT_FOUND_ERROR = "Challenge with id: %s not found"; + UUID challengeUuid = UUID.randomUUID(); + + when(challengeRepository.findByUuid(challengeUuid)).thenReturn(Mono.empty()); + + StepVerifier.create(challengeService.addChallengeToSolved(challengeUuid.toString(), UUID.randomUUID().toString())) + .expectErrorMatches(error -> + error instanceof ChallengeNotFoundReturn404Exception && + error.getMessage().equals(String.format(CHALLENGE_NOT_FOUND_ERROR, challengeUuid.toString()))) + .verify(); + + verify(challengeRepository, times(1)).findByUuid(challengeUuid); + } + @Test void addChallengeToFavorites_WhenUserNotFound_ReturnsError() { UUID challengeUuid = UUID.randomUUID(); @@ -1025,6 +1150,10 @@ public static Stream addChallengeToBookmarks_WhenNotAddedAndTimesBookma return Stream.of(null, 0); } + public static Stream addChallengeToSolved_WhenNotAddedAndTimesSolvedIsNullOrZero_SetTimesSolvedToOneAndReturnsSolvedDTO() { + return Stream.of(null, 0); + } + @Test void removeChallengeFromFavorites_WhenChallengeUuidNotValid_ReturnsError() { StepVerifier.create(challengeService.removeChallengeFromFavorites("InvalidUuid", UUID.randomUUID().toString())) @@ -1320,7 +1449,7 @@ void getChallengesByFilter_ValidParams_FiltersAppliedCorrectly() { Set.of(new LanguageDocument(languageId, "Language Name", "image.png")), List.of(UUID.randomUUID()), Topic.COMPONENTS, - 0, 0, tags + 0, 0, 0, tags ); when(challengeRepository.findAllByUuidNotNullExcludingTestingValues()) @@ -1374,7 +1503,7 @@ void getChallengesByFilter_NoLanguage_FilterAppliedCorrectly() { Set.of(new LanguageDocument(UUID.randomUUID(), "Other Language", "image.png")), List.of(UUID.randomUUID()), Topic.COMPONENTS, - 0, 0, tags + 0, 0, 0, tags ); when(challengeRepository.findAllByUuidNotNullExcludingTestingValues()) diff --git a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/service/ResourceServiceImplTest.java b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/service/ResourceServiceImplTest.java index 2a2bba86d..dd366dd3a 100644 --- a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/service/ResourceServiceImplTest.java +++ b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/service/ResourceServiceImplTest.java @@ -7,6 +7,9 @@ import com.itachallenge.challenge.enums.AssociationType; import com.itachallenge.challenge.enums.ResourceContentType; import com.itachallenge.challenge.enums.Topic; +import com.itachallenge.challenge.exception.BadRequestException; +import com.itachallenge.challenge.exception.InternalServerErrorException; +import com.itachallenge.challenge.exception.ResourceNotFoundException; import com.itachallenge.challenge.helper.DocumentToDtoConverter; import com.itachallenge.challenge.repository.ResourceRepository; import org.junit.jupiter.api.Test; @@ -14,6 +17,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import static org.mockito.ArgumentMatchers.*; @@ -416,5 +420,101 @@ void createResource_WithFailedConversion_ShouldThrowError() { .verify(); } + // ID CON recursos + @Test + void getResourcesByChallengeId_WhenResourcesExist_ReturnsFluxOfResources() { + + UUID challengeId = UUID.randomUUID(); + UUID resourceId1 = UUID.randomUUID(); + UUID resourceId2 = UUID.randomUUID(); + + + ResourceDocument doc1 = new ResourceDocument( + resourceId1, "Resource 1", "Desc 1", "https://example.com/1", + Topic.DEBUGGING, ResourceContentType.VIDEO, List.of(challengeId), AssociationType.ALLSAMETOPIC + ); + ResourceDocument doc2 = new ResourceDocument( + resourceId2, "Resource 2", "Desc 2", "https://example.com/2", + Topic.COMPONENTS, ResourceContentType.BLOG, List.of(challengeId), AssociationType.CHOOSE + ); + + ResourceDto dto1 = new ResourceDto(); + dto1.setResourceId(resourceId1); + dto1.setTitle("Resource 1"); + + + ResourceDto dto2 = new ResourceDto(); + dto2.setResourceId(resourceId2); + dto2.setTitle("Resource 2"); + + + when(resourceRepository.findByChallengeIdsContaining(challengeId)) + .thenReturn(Flux.just(doc1, doc2)); + when(resourceConverter.convertDocumentToDto(doc1, ResourceDto.class)).thenReturn(dto1); + when(resourceConverter.convertDocumentToDto(doc2, ResourceDto.class)).thenReturn(dto2); + + + StepVerifier.create(resourceService.getResourcesByChallengeId(challengeId)) + .expectNext(dto1) + .expectNext(dto2) + .verifyComplete(); + } + + @Test + void getResourcesByChallengeId_WhenNoResourcesExist_ThrowsResourceNotFoundException() { + + UUID challengeId = UUID.randomUUID(); + when(resourceRepository.findByChallengeIdsContaining(challengeId)) + .thenReturn(Flux.empty()); // Simula que no hay recursos + + StepVerifier.create(resourceService.getResourcesByChallengeId(challengeId)) + .expectError(ResourceNotFoundException.class) + .verify(); + } + + @Test + void getResourcesByChallengeId_WhenIdIsNull_ThrowsBadRequestException() { + StepVerifier.create(resourceService.getResourcesByChallengeId(null)) + .expectErrorMatches(ex -> + ex instanceof BadRequestException && + ex.getMessage().equals("Challenge ID cannot be null") + ) + .verify(); + } + + + @Test + void getResourcesByChallengeId_WhenRepositoryFails_ThrowsInternalServerErrorException() { + UUID challengeId = UUID.randomUUID(); + when(resourceRepository.findByChallengeIdsContaining(challengeId)) + .thenReturn(Flux.error(new RuntimeException("DB Connection Failed"))); + + StepVerifier.create(resourceService.getResourcesByChallengeId(challengeId)) + .expectErrorMatches(ex -> + ex instanceof InternalServerErrorException && + ex.getMessage().equals("Failed to fetch resources for challenge ID: " + challengeId) + ) + .verify(); + + verify(resourceRepository, times(1)).findByChallengeIdsContaining(challengeId); + } + + @Test + void getResourcesByChallengeId_WhenResourceNotFound_PropagatesException() { + UUID challengeId = UUID.randomUUID(); + ResourceNotFoundException ex = new ResourceNotFoundException("Not found"); + when(resourceRepository.findByChallengeIdsContaining(challengeId)) + .thenReturn(Flux.error(ex)); + + StepVerifier.create(resourceService.getResourcesByChallengeId(challengeId)) + .expectErrorMatches(thrownEx -> + thrownEx instanceof ResourceNotFoundException && + thrownEx.getMessage().equals("Not found") + ) + .verify(); + } + + + } diff --git a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/service/UserServiceImplTest.java b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/service/UserServiceImplTest.java index b3a76d4ee..b0d6a0f4e 100644 --- a/itachallenge-challenge/src/test/java/com/itachallenge/challenge/service/UserServiceImplTest.java +++ b/itachallenge-challenge/src/test/java/com/itachallenge/challenge/service/UserServiceImplTest.java @@ -24,8 +24,10 @@ public class UserServiceImplTest { private static final String FAVORITES_URL = "/itachallenge/api/v1/user/users/%s/favorites/%s"; private static final String BOOKMARKS_URL = "/itachallenge/api/v1/user/users/%s/bookmarks/%s"; + private static final String SOLVED_URL = "/itachallenge/api/v1/user/users/%s/solved/%s"; public static final String X_FAVORITE_MESSAGE = "X-Favorite-Message"; public static final String X_BOOKMARK_MESSAGE = "X-Bookmark-Message"; + public static final String X_SOLVED_MESSAGE = "X-Solved-Message"; @BeforeEach void setUp() throws IOException { @@ -90,6 +92,29 @@ void addChallengeToBookmarks_AddedToUser_ReturnsTrue() throws InterruptedExcepti assertEquals("POST", request.getMethod()); } + @Test + void addChallengeToSolved_AddedToUser_ReturnsTrue() throws InterruptedException { + String userId = "someId"; + String challengeId = "anotherId"; + + mockWebServer.enqueue(new MockResponse() + .setBody("true") + .setResponseCode(201) + .addHeader("Content-Type", "application/json")); + + Mono result = userService.addChallengeToSolved(userId, challengeId); + + StepVerifier.create(result) + .expectNext(true) + .verifyComplete(); + + RecordedRequest request = mockWebServer.takeRequest(); + assertNotNull(request.getRequestUrl()); + assertEquals( + String.format(SOLVED_URL, userId, challengeId), + request.getRequestUrl().encodedPath()); + } + @Test void addChallengeToFavorites_NotAddedToUser_ReturnsFalse() throws InterruptedException { String userId = "someId"; @@ -137,6 +162,29 @@ void addChallengeToBookmarks_NotAddedToUser_ReturnsFalse() throws InterruptedExc assertEquals("POST", request.getMethod()); } + @Test + void addChallengeToSolved_NotAddedToUser_ReturnsFalse() throws InterruptedException { + String userId = "someId"; + String challengeId = "anotherId"; + + mockWebServer.enqueue(new MockResponse() + .setBody("false") + .setResponseCode(200) + .addHeader("Content-Type", "application/json")); + + Mono result = userService.addChallengeToSolved(userId, challengeId); + + StepVerifier.create(result) + .expectNext(false) + .verifyComplete(); + + RecordedRequest request = mockWebServer.takeRequest(); + assertNotNull(request.getRequestUrl()); + assertEquals( + String.format(SOLVED_URL, userId, challengeId), + request.getRequestUrl().encodedPath()); + } + @Test void addChallengeToFavorites_BadRequest_ReturnsError() throws InterruptedException { String userId = "someId"; @@ -194,6 +242,34 @@ void addChallengeToBookmarks_BadRequest_ReturnsError() throws InterruptedExcepti assertEquals("POST", request.getMethod()); } + @Test + void addChallengeToSolved_BadRequest_ReturnsError() throws InterruptedException { + String userId = "someId"; + String challengeId = "anotherId"; + String someErrorMessage = "Some error message"; + + mockWebServer.enqueue(new MockResponse() + .setBody("false") + .setResponseCode(400) + .addHeader(X_SOLVED_MESSAGE, someErrorMessage) + .addHeader("Content-Type", "application/json")); + + Mono result = userService.addChallengeToSolved(userId, challengeId); + + StepVerifier.create(result) + .expectErrorSatisfies(throwable -> { + assertInstanceOf(BadRequestException.class, throwable); + assertTrue(throwable.getMessage().contains(someErrorMessage)); + }) + .verify(); + + RecordedRequest request = mockWebServer.takeRequest(); + assertNotNull(request.getRequestUrl()); + assertEquals( + String.format(SOLVED_URL, userId, challengeId), + request.getRequestUrl().encodedPath()); + } + @Test void addChallengeToFavorites_UserNotFound_ReturnsError() throws InterruptedException { String userId = "someId"; @@ -247,6 +323,32 @@ void addChallengeToBookmarks_UserNotFound_ReturnsError() throws InterruptedExcep assertEquals("POST", request.getMethod()); } + @Test + void addChallengeToSolved_UserNotFound_ReturnsError() throws InterruptedException { + String userId = "someId"; + String challengeId = "anotherId"; + + mockWebServer.enqueue(new MockResponse() + .setBody("false") + .setResponseCode(404) + .addHeader("Content-Type", "application/json")); + + Mono result = userService.addChallengeToSolved(userId, challengeId); + + StepVerifier.create(result) + .expectErrorSatisfies(throwable -> { + assertInstanceOf(UserNotFoundException.class, throwable); + assertTrue(throwable.getMessage().contains("User not found")); + }) + .verify(); + + RecordedRequest request = mockWebServer.takeRequest(); + assertNotNull(request.getRequestUrl()); + assertEquals( + String.format(SOLVED_URL, userId, challengeId), + request.getRequestUrl().encodedPath()); + } + @Test void addChallengeToFavorites_500_ReturnsError() throws InterruptedException { String userId = "someId"; @@ -304,6 +406,34 @@ void addChallengeToBookmarks_500_ReturnsError() throws InterruptedException { assertEquals("POST", request.getMethod()); } + @Test + void addChallengeToSolved_500_ReturnsError() throws InterruptedException { + String userId = "someId"; + String challengeId = "anotherId"; + String someErrorMessage = "Some error message"; + + mockWebServer.enqueue(new MockResponse() + .setBody("false") + .setResponseCode(500) + .addHeader(X_SOLVED_MESSAGE, someErrorMessage) + .addHeader("Content-Type", "application/json")); + + Mono result = userService.addChallengeToSolved(userId, challengeId); + + StepVerifier.create(result) + .expectErrorSatisfies(throwable -> { + assertInstanceOf(InternalServerErrorException.class, throwable); + assertTrue(throwable.getMessage().contains(someErrorMessage)); + }) + .verify(); + + RecordedRequest request = mockWebServer.takeRequest(); + assertNotNull(request.getRequestUrl()); + assertEquals( + String.format(SOLVED_URL, userId, challengeId), + request.getRequestUrl().encodedPath()); + } + @Test void removeChallengeFromFavorites_DeletedFromUser_ReturnsTrue() throws InterruptedException { String userId = "someId"; diff --git a/itachallenge-user/src/main/java/com/itachallenge/user/controller/UserController.java b/itachallenge-user/src/main/java/com/itachallenge/user/controller/UserController.java index c4380d5fe..2da6a70e3 100755 --- a/itachallenge-user/src/main/java/com/itachallenge/user/controller/UserController.java +++ b/itachallenge-user/src/main/java/com/itachallenge/user/controller/UserController.java @@ -370,4 +370,32 @@ public Mono>> getUserFavorites(@PathVariable String use return ResponseEntity.ok().body(favorites); }); } + + @Operation( + summary = "Gets challenges marked as bookmarks by a user", + description = "Returns a set of challenge IDs that the specified user has marked as bookmarked", + parameters = { + @Parameter( + name = "userId", + description = "UUID of the user", + required = true, + in = ParameterIn.PATH + ) + }, + responses = { + @ApiResponse(responseCode = "200", description = "Set of bookmarked challengeIds by user"), + @ApiResponse(responseCode = "404", description = "User not found"), + @ApiResponse(responseCode = "400", description = "The provided IDs are not valid."), + @ApiResponse(responseCode = "500", description = "Unexpected error") + } + ) + + @GetMapping("/users/{userId}/bookmarks") + public Mono>> getUserBookmarks(@PathVariable String userId) { + return userService.getUserBookmarks(userId) + .map(bookmarks -> { + log.info("Retrieved {} bookmarked challenges for user {}", bookmarks.size(), userId); + return ResponseEntity.ok().body(bookmarks); + }); + } } \ No newline at end of file diff --git a/itachallenge-user/src/main/java/com/itachallenge/user/document/enums/ChallengeStatus.java b/itachallenge-user/src/main/java/com/itachallenge/user/document/enums/ChallengeStatus.java index 2ce61bbbe..344f9549d 100644 --- a/itachallenge-user/src/main/java/com/itachallenge/user/document/enums/ChallengeStatus.java +++ b/itachallenge-user/src/main/java/com/itachallenge/user/document/enums/ChallengeStatus.java @@ -2,8 +2,11 @@ import lombok.Getter; +import java.util.Arrays; + @Getter -public enum ChallengeStatus {ENDED("ENDED"); +public enum ChallengeStatus {ENDED("ENDED"), + IN_PROGRESS("IN_PROGRESS"); private final String value; @@ -11,9 +14,15 @@ public enum ChallengeStatus {ENDED("ENDED"); this.value = value; } - public static ChallengeStatus determineChallengeStatus(String status){ - return status != null && status.equalsIgnoreCase(ChallengeStatus.ENDED.getValue()) ? - ChallengeStatus.ENDED : null; + public static ChallengeStatus challengeStatusFromString(String status) { + ChallengeStatus output = null; + if (status != null) { + output = Arrays.stream(ChallengeStatus.values()) + .filter(s -> status.equalsIgnoreCase(s.getValue())) + .findFirst() + .orElse(null); + } + return output; } } diff --git a/itachallenge-user/src/main/java/com/itachallenge/user/service/UserService.java b/itachallenge-user/src/main/java/com/itachallenge/user/service/UserService.java index 51f36bcc3..a3c2bd2ac 100644 --- a/itachallenge-user/src/main/java/com/itachallenge/user/service/UserService.java +++ b/itachallenge-user/src/main/java/com/itachallenge/user/service/UserService.java @@ -18,4 +18,6 @@ public interface UserService { Mono> getUserFavorites(String userId); Mono deleteChallengeFromBookmarks(String userId, String challengeId); + + Mono> getUserBookmarks(String userId); } \ No newline at end of file diff --git a/itachallenge-user/src/main/java/com/itachallenge/user/service/UserServiceImpl.java b/itachallenge-user/src/main/java/com/itachallenge/user/service/UserServiceImpl.java index 97228e9d0..5a84c478e 100644 --- a/itachallenge-user/src/main/java/com/itachallenge/user/service/UserServiceImpl.java +++ b/itachallenge-user/src/main/java/com/itachallenge/user/service/UserServiceImpl.java @@ -159,4 +159,14 @@ public Mono> getUserFavorites(String userId) { ); } + @Override + public Mono> getUserBookmarks(String userId) { + return parseAndValidateUUID(userId) + .flatMap(userUuid -> + userRepository.findById(userUuid) + .switchIfEmpty(Mono.error(new NotFoundException("User not found with id: " + userId))) + .map(user -> Optional.ofNullable(user.getBookmarkChallenges()).orElseGet(HashSet::new)) + ); + } + } diff --git a/itachallenge-user/src/main/java/com/itachallenge/user/service/UserSolutionServiceImpl.java b/itachallenge-user/src/main/java/com/itachallenge/user/service/UserSolutionServiceImpl.java index 2a5ab0795..e0970ab52 100644 --- a/itachallenge-user/src/main/java/com/itachallenge/user/service/UserSolutionServiceImpl.java +++ b/itachallenge-user/src/main/java/com/itachallenge/user/service/UserSolutionServiceImpl.java @@ -11,7 +11,6 @@ import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; -import java.util.List; import java.util.UUID; @Service @@ -37,7 +36,7 @@ public Mono addSolution(UserSolutionRequestDto userSolu .solutionText(userSolutionDto.getSolutionText()) .build(); - challengeStatus = ChallengeStatus.determineChallengeStatus(status); + challengeStatus = ChallengeStatus.challengeStatusFromString(status); if (challengeStatus == null) { log.error("PUT operation failed due to invalid challenge status parameter"); diff --git a/itachallenge-user/src/test/java/com/itachallenge/user/controller/UserControllerTest.java b/itachallenge-user/src/test/java/com/itachallenge/user/controller/UserControllerTest.java index 5b58a6cf5..77cbe840d 100644 --- a/itachallenge-user/src/test/java/com/itachallenge/user/controller/UserControllerTest.java +++ b/itachallenge-user/src/test/java/com/itachallenge/user/controller/UserControllerTest.java @@ -498,4 +498,75 @@ void getUserFavorites_returns500IfUnexpectedError() { verify(userService, times(1)).getUserFavorites(userId.toString()); } + @Test + @DisplayName("GET /users/{userId}/bookmarks returns bookmarked challenges") + void getUserBookmarks_returnsBookmarks() { + UUID userId = UUID.randomUUID(); + Set expectedBookmarks = Set.of(UUID.randomUUID(), UUID.randomUUID()); + + when(userService.getUserBookmarks(userId.toString())).thenReturn(Mono.just(expectedBookmarks)); + + webTestClient.get() + .uri("/itachallenge/api/v1/user/users/{userId}/bookmarks", userId) + .exchange() + .expectStatus().isOk() + .expectBodyList(UUID.class) + .hasSize(expectedBookmarks.size()) + .contains(expectedBookmarks.toArray(new UUID[0])); + + verify(userService, times(1)).getUserBookmarks(userId.toString()); + } + + @Test + @DisplayName("GET /users/{userId}/bookmarks returns 404 if user not found") + void getUserBookmarks_returns404IfUserNotFound() { + UUID userId = UUID.randomUUID(); + + when(userService.getUserBookmarks(userId.toString())) + .thenReturn(Mono.error(new NotFoundException("User not found"))); + + webTestClient.get() + .uri("/itachallenge/api/v1/user/users/{userId}/bookmarks", userId) + .exchange() + .expectStatus().isNotFound() + .expectBody(String.class).isEqualTo("User not found"); + + verify(userService, times(1)).getUserBookmarks(userId.toString()); + } + + @Test + @DisplayName("GET /users/{userId}/bookmarks returns 400 if UUID is invalid") + void getUserBookmarks_returns400IfInvalidUUID() { + String invalidUserId = "invalid-uuid"; + + when(userService.getUserBookmarks(invalidUserId)) + .thenReturn(Mono.error(new BadUUIDException("The provided IDs are not valid."))); + + webTestClient.get() + .uri("/itachallenge/api/v1/user/users/{userId}/bookmarks", invalidUserId) + .exchange() + .expectStatus().isBadRequest() + .expectBody(String.class).isEqualTo("The provided IDs are not valid."); + + verify(userService, times(1)).getUserBookmarks(invalidUserId); + } + + @Test + @DisplayName("GET /users/{userId}/bookmarks returns 500 if there is an internal error") + void getUserBookmarks_returns500IfUnexpectedError() { + UUID userId = UUID.randomUUID(); + + when(userService.getUserBookmarks(userId.toString())) + .thenReturn(Mono.error(new RuntimeException("Unexpected error"))); + + webTestClient.get() + .uri("/itachallenge/api/v1/user/users/{userId}/bookmarks", userId) + .exchange() + .expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + .expectBody(String.class).isEqualTo("Unexpected error happened."); + + verify(userService, times(1)).getUserBookmarks(userId.toString()); + } + + } diff --git a/itachallenge-user/src/test/java/com/itachallenge/user/service/UserServiceImplTest.java b/itachallenge-user/src/test/java/com/itachallenge/user/service/UserServiceImplTest.java index 99c6b3c4a..dc2816dcb 100644 --- a/itachallenge-user/src/test/java/com/itachallenge/user/service/UserServiceImplTest.java +++ b/itachallenge-user/src/test/java/com/itachallenge/user/service/UserServiceImplTest.java @@ -727,4 +727,69 @@ void getUserFavorites_WhenInvalidUUID_ReturnsBadUUIDException() { .verify(); } + @Test + @DisplayName("getUserBookmarks returns bookmarked challenges when the user exists and has challenges") + void getUserBookmarks_WhenUserExistsWithBookmarks_ReturnsSet() { + UUID userId = UUID.randomUUID(); + UUID challengeId1 = UUID.randomUUID(); + UUID challengeId2 = UUID.randomUUID(); + + Set bookmarks = Set.of(challengeId1, challengeId2); + UserDocument user = new UserDocument(); + user.setUuid(userId); + user.setBookmarkChallenges(bookmarks); + + when(userRepository.findById(userId)).thenReturn(Mono.just(user)); + + userService.getUserBookmarks(userId.toString()) + .as(StepVerifier::create) + .expectNextMatches(result -> result.size() == 2 && result.contains(challengeId1)) + .verifyComplete(); + } + + @Test + @DisplayName("getUserBookmarks returns an empty set when the user has no challenges marked.") + void getUserBookmarks_WhenUserHasNoBookmarks_ReturnsEmptySet() { + UUID userId = UUID.randomUUID(); + + UserDocument user = new UserDocument(); + user.setUuid(userId); + user.setFavoriteChallenges(null); // explícitely null + + when(userRepository.findById(userId)).thenReturn(Mono.just(user)); + + userService.getUserBookmarks(userId.toString()) + .as(StepVerifier::create) + .expectNextMatches(Set::isEmpty) + .verifyComplete(); + } + + @Test + @DisplayName("getUserBookmarks returns NotFoundException error when the user does not exist") + void getUserBookmarks_WhenUserNotFound_ReturnsError() { + UUID userId = UUID.randomUUID(); + + when(userRepository.findById(userId)).thenReturn(Mono.empty()); + + userService.getUserBookmarks(userId.toString()) + .as(StepVerifier::create) + .expectErrorMatches(error -> + error instanceof NotFoundException && + error.getMessage().equals("User not found with id: " + userId)) + .verify(); + } + + @Test + @DisplayName("getUserBookmarks throws BadUUIDException when the UUID format is invalid") + void getUserBookmarks_WhenInvalidUUID_ReturnsBadUUIDException() { + String invalidUUID = "invalid-uuid"; + + userService.getUserBookmarks(invalidUUID) + .as(StepVerifier::create) + .expectErrorMatches(error -> + error instanceof BadUUIDException && + error.getMessage().equals("Invalid ID format")) + .verify(); + } + }