Repository: IT-Academy-BCN/ita-challenges-backend
Pull request #831: Feature/205 bookmark delete endpoint user microservice
Author: login: AlexandraBonetCanela
Created: 2025-04-03T15:10:07Z | Merged: 2025-04-07T11:32:09Z

Added bookmark DELETE endpoint following same favorite endpoint fashion.

Also fix favorite DELETE endpoint 201 Created response to return 200.


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d0078e07b..279bd0381 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 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). 
 
+* Issue #831: Added DELETE endpoint for user's bookmals in User Microservice.
 * Issue #826: Added GET endpoint to retrieve the list of challenges marked as favorites by a user.                                          
 * Issue #763: Modified POST endpoint for adding new challenge (in Challenge micro)  
               Created language image attribute in LanguageDocument and LanguageDto.
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 7fd8babc0..b9314015b 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
@@ -34,9 +34,10 @@ public class UserController {
     public static final String X_FAVORITE_ADDED = "X-Favorite-Added";
     public static final String X_FAVORITE_DELETED = "X-Favorite-Deleted";
     public static final String X_FAVORITE_MESSAGE = "X-Favorite-Message";
-    public static final String X_VALIDATION_STATUS = "X-Validation-Status";
     public static final String X_BOOKMARK_ADDED = "X-Bookmark-Added";
+    public static final String X_BOOKMARK_DELETED = "X-Bookmark-Deleted";
     public static final String X_BOOKMARK_MESSAGE = "X-Bookmark-Message";
+    public static final String X_VALIDATION_STATUS = "X-Validation-Status";
     public static final String X_ERROR_MESSAGE = "X-Error-Message";
     public static final String X_GITHUB_USERNAME ="X-Github-Username";
     public static final String FALSE = "False";
@@ -332,12 +333,7 @@ public Mono<ResponseEntity<Boolean>> addToBookmarks(@PathVariable String userId,
             responses = {
                     @ApiResponse(
                             responseCode = "200",
-                            description = "No change, challenge was not in favorites",
-                            content = @Content(mediaType = "application/json")
-                    ),
-                    @ApiResponse(
-                            responseCode = "201",
-                            description = "Challenge deleted from favorites",
+                            description = "Challenge deleted from favorites or was not in favorites",
                             content = @Content(mediaType = "application/json")
                     ),
                     @ApiResponse(
@@ -363,7 +359,7 @@ public Mono<ResponseEntity<Boolean>> deleteFromFavorites(@PathVariable String us
                 .map(deleted -> {
                     if (Boolean.TRUE.equals(deleted)) {
                         log.info("Challenge '{}' deleted from user '{}' favorites", challengeId, userId);
-                        return ResponseEntity.status(HttpStatus.CREATED)
+                        return ResponseEntity.ok()
                                 .header(X_FAVORITE_DELETED, "True")
                                 .header(X_FAVORITE_MESSAGE, "Challenge deleted from favorites.")
                                 .body(true);
@@ -398,6 +394,87 @@ public Mono<ResponseEntity<Boolean>> deleteFromFavorites(@PathVariable String us
 
     }
 
+    @Operation(
+            summary = "Delete Challenge from User Bookmark Challenges",
+            description = "Deletes challenge from user bookmarks",
+            parameters = {
+                    @Parameter(
+                            name = "userId",
+                            description = "User ID",
+                            required = true,
+                            in = ParameterIn.PATH
+                    ),
+                    @Parameter(
+                            name = "challengeId",
+                            description = "Challenge ID",
+                            required = true,
+                            in = ParameterIn.PATH
+                    )
+            },
+            responses = {
+                    @ApiResponse(
+                            responseCode = "200",
+                            description = "Challenge deleted from bookmarks or was not in bookmarks",
+                            content = @Content(mediaType = "application/json")
+                    ),
+                    @ApiResponse(
+                            responseCode = "400",
+                            description = "Bad Request. The provided IDs have a bad format",
+                            content = @Content(mediaType = "application/json")
+                    ),
+                    @ApiResponse(
+                            responseCode = "404",
+                            description = "Not found. No user is found with the provided user id.",
+                            content = @Content(mediaType = "application/json")
+                    ),
+                    @ApiResponse(
+                            responseCode = "500",
+                            description = "Internal server error. An unexpected error occurred.",
+                            content = @Content(mediaType = "application/json")
+                    )
+            }
+    )
+    @DeleteMapping("/users/{userId}/bookmarks/{challengeId}")
+    public Mono<ResponseEntity<Boolean>> deleteFromBookmarks(@PathVariable String userId, @PathVariable String challengeId) {
+        return userService.deleteChallengeFromBookmarks(userId, challengeId)
+                .map(deleted -> {
+                    if (Boolean.TRUE.equals(deleted)) {
+                        log.info("Challenge '{}' deleted from user '{}' bookmarks", challengeId, userId);
+                        return ResponseEntity.ok()
+                                .header(X_BOOKMARK_DELETED, "True")
+                                .header(X_BOOKMARK_MESSAGE, "Challenge deleted from bookmarks.")
+                                .body(true);
+                    }
+                    log.info("No change, User's '{}' bookmarks doesn't contain Challenge '{}'", userId, challengeId);
+                    return ResponseEntity.ok()
+                            .header(X_BOOKMARK_DELETED, FALSE)
+                            .header(X_BOOKMARK_MESSAGE, "Challenge not found in user's bookmarks.")
+                            .body(false);
+                })
+                .onErrorResume(throwable -> {
+                    if (throwable instanceof NotFoundException) {
+                        log.error("No User not found with id: {}", userId);
+                        return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND)
+                                .header(X_BOOKMARK_DELETED, FALSE)
+                                .header(X_BOOKMARK_MESSAGE, "User not found.")
+                                .body(false));
+                    }
+                    if (throwable instanceof BadUUIDException) {
+                        log.error("The provided IDs are not valid.");
+                        return Mono.just(ResponseEntity.status(HttpStatus.BAD_REQUEST)
+                                .header(X_BOOKMARK_DELETED, FALSE)
+                                .header(X_BOOKMARK_MESSAGE, "The provided IDs are not valid.")
+                                .body(false));
+                    }
+                    log.error("Unexpected error: {}", throwable.getMessage());
+                    return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+                            .header(X_BOOKMARK_DELETED, FALSE)
+                            .header(X_BOOKMARK_MESSAGE, "Unexpected server error.")
+                            .body(false));
+                });
+
+    }
+
     @Operation(
             summary = "Gets challenges marked as favorites by a user",
             description = "Returns a set of challenge IDs that the specified user has marked as favorites",
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 5d6bb130b..51f36bcc3 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
@@ -17,6 +17,5 @@ public interface UserService {
 
     Mono<Set<UUID>> getUserFavorites(String userId);
 
-
-
-}
+    Mono<Boolean> deleteChallengeFromBookmarks(String userId, String challengeId);
+}
\ 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 d35bab719..b092f3eee 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
@@ -66,6 +66,19 @@ public Mono<Boolean> deleteChallengeFromFavorites(String userId, String challeng
                 });
     }
 
+    @Override
+    public Mono<Boolean> deleteChallengeFromBookmarks(String userId, String challengeId) {
+        return Mono.zip(parseAndValidateUUID(userId), parseAndValidateUUID(challengeId))
+                .flatMap(uuidTuple -> {
+                    UUID userUuid = uuidTuple.getT1();
+                    UUID challengeUuid = uuidTuple.getT2();
+
+                    return userRepository.findById(userUuid)
+                            .switchIfEmpty(Mono.error(new NotFoundException("User not found")))
+                            .flatMap(user -> deleteFromBookmarks(user, challengeUuid));
+                });
+    }
+
     private Mono<Boolean> addToFavorites(UserDocument user, UUID challengeUuid) {
         Set<UUID> favorites = Optional.ofNullable(user.getFavoriteChallenges())
                 .orElseGet(HashSet::new);
@@ -108,6 +121,20 @@ private Mono<Boolean> deleteFromFavorites(UserDocument user, UUID challengeUuid)
         return Mono.just(false);
     }
 
+    private Mono<Boolean> deleteFromBookmarks(UserDocument user, UUID challengeUuid) {
+        Set<UUID> bookmarks = Optional.ofNullable(user.getBookmarkChallenges())
+                .orElseGet(HashSet::new);
+
+        boolean deleted = bookmarks.remove(challengeUuid);
+
+        if (deleted) {
+            user.setBookmarkChallenges(bookmarks);
+            return userRepository.save(user).then(Mono.just(true));
+        }
+
+        return Mono.just(false);
+    }
+
     private Mono<UUID> parseAndValidateUUID(String id) {
 
         if (id == null || id.isEmpty()) {
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 d320b9c2f..20927b67f 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
@@ -288,7 +288,7 @@ void addToBookmarks_WhenUnexpectedError_Returns500() {
     }
 
     @Test
-    void deleteFromFavorites_WhenDeleted_Returns201() {
+    void deleteFromFavorites_WhenDeleted_Returns200() {
         String userId = UUID.randomUUID().toString();
         String challengeId = UUID.randomUUID().toString();
         when(userService.deleteChallengeFromFavorites(userId, challengeId))
@@ -297,7 +297,7 @@ void deleteFromFavorites_WhenDeleted_Returns201() {
         webTestClient.delete()
                 .uri("/itachallenge/api/v1/user/users/" + userId + "/favorites/" + challengeId)
                 .exchange()
-                .expectStatus().isEqualTo(HttpStatus.CREATED)
+                .expectStatus().isEqualTo(HttpStatus.OK)
                 .expectHeader().valueEquals("X-Favorite-Deleted", "True")
                 .expectHeader().valueEquals("X-Favorite-Message", "Challenge deleted from favorites.")
                 .expectBody(Boolean.class).isEqualTo(true);
@@ -305,6 +305,24 @@ void deleteFromFavorites_WhenDeleted_Returns201() {
         verify(userService, times(1)).deleteChallengeFromFavorites(userId, challengeId);
     }
 
+    @Test
+    void deleteFromBookmarks_WhenDeleted_Returns200() {
+        String userId = UUID.randomUUID().toString();
+        String challengeId = UUID.randomUUID().toString();
+        when(userService.deleteChallengeFromBookmarks(userId, challengeId))
+                .thenReturn(Mono.just(true));
+
+        webTestClient.delete()
+                .uri("/itachallenge/api/v1/user/users/" + userId + "/bookmarks/" + challengeId)
+                .exchange()
+                .expectStatus().isEqualTo(HttpStatus.OK)
+                .expectHeader().valueEquals("X-Bookmark-Deleted", "True")
+                .expectHeader().valueEquals("X-Bookmark-Message", "Challenge deleted from bookmarks.")
+                .expectBody(Boolean.class).isEqualTo(true);
+
+        verify(userService, times(1)).deleteChallengeFromBookmarks(userId, challengeId);
+    }
+
     @Test
     void deleteFromFavorites_WhenNotInFavorites_Returns200() {
         String userId = UUID.randomUUID().toString();
@@ -323,6 +341,24 @@ void deleteFromFavorites_WhenNotInFavorites_Returns200() {
         verify(userService, times(1)).deleteChallengeFromFavorites(userId, challengeId);
     }
 
+    @Test
+    void deleteFromBookmarks_WhenNotInBookmarks_Returns200() {
+        String userId = UUID.randomUUID().toString();
+        String challengeId = UUID.randomUUID().toString();
+        when(userService.deleteChallengeFromBookmarks(userId, challengeId))
+                .thenReturn(Mono.just(false));
+
+        webTestClient.delete()
+                .uri("/itachallenge/api/v1/user/users/" + userId + "/bookmarks/" + challengeId)
+                .exchange()
+                .expectStatus().isEqualTo(HttpStatus.OK)
+                .expectHeader().valueEquals("X-Bookmark-Deleted", "False")
+                .expectHeader().valueEquals("X-Bookmark-Message", "Challenge not found in user's bookmarks.")
+                .expectBody(Boolean.class).isEqualTo(false);
+
+        verify(userService, times(1)).deleteChallengeFromBookmarks(userId, challengeId);
+    }
+
     @Test
     void deleteFromFavorites_WhenUserNotExists_Returns404() {
         String userId = UUID.randomUUID().toString();
@@ -341,6 +377,24 @@ void deleteFromFavorites_WhenUserNotExists_Returns404() {
         verify(userService, times(1)).deleteChallengeFromFavorites(userId, challengeId);
     }
 
+    @Test
+    void deleteFromBookmarks_WhenUserNotExists_Returns404() {
+        String userId = UUID.randomUUID().toString();
+        String challengeId = UUID.randomUUID().toString();
+        when(userService.deleteChallengeFromBookmarks(userId, challengeId))
+                .thenReturn(Mono.error(new NotFoundException("User not found")));
+
+        webTestClient.delete()
+                .uri("/itachallenge/api/v1/user/users/" + userId + "/bookmarks/" + challengeId)
+                .exchange()
+                .expectStatus().isEqualTo(HttpStatus.NOT_FOUND)
+                .expectHeader().valueEquals("X-Bookmark-Deleted", "False")
+                .expectHeader().valueEquals("X-Bookmark-Message", "User not found.")
+                .expectBody(Boolean.class).isEqualTo(false);
+
+        verify(userService, times(1)).deleteChallengeFromBookmarks(userId, challengeId);
+    }
+
     @Test
     void deleteFromFavorites_WhenBadFormattedId_Returns404() {
         String userId = "invalidUuid";
@@ -359,6 +413,24 @@ void deleteFromFavorites_WhenBadFormattedId_Returns404() {
         verify(userService, times(1)).deleteChallengeFromFavorites(userId, challengeId);
     }
 
+    @Test
+    void deleteFromBookmarks_WhenBadFormattedId_Returns404() {
+        String userId = "invalidUuid";
+        String challengeId = "invalidUUid";
+        when(userService.deleteChallengeFromBookmarks(userId, challengeId))
+                .thenReturn(Mono.error(new BadUUIDException("Error message")));
+
+        webTestClient.delete()
+                .uri("/itachallenge/api/v1/user/users/" + userId + "/bookmarks/" + challengeId)
+                .exchange()
+                .expectStatus().isEqualTo(HttpStatus.BAD_REQUEST)
+                .expectHeader().valueEquals("X-Bookmark-Deleted", "False")
+                .expectHeader().valueEquals("X-Bookmark-Message", "The provided IDs are not valid.")
+                .expectBody(Boolean.class).isEqualTo(false);
+
+        verify(userService, times(1)).deleteChallengeFromBookmarks(userId, challengeId);
+    }
+
     @Test
     void deleteFromFavorites_WhenUnexpectedError_Returns500() {
         String userId = UUID.randomUUID().toString();
@@ -377,6 +449,24 @@ void deleteFromFavorites_WhenUnexpectedError_Returns500() {
         verify(userService, times(1)).deleteChallengeFromFavorites(userId, challengeId);
     }
 
+    @Test
+    void deleteFromBookmarks_WhenUnexpectedError_Returns500() {
+        String userId = UUID.randomUUID().toString();
+        String challengeId = UUID.randomUUID().toString();
+        when(userService.deleteChallengeFromBookmarks(userId, challengeId))
+                .thenReturn(Mono.error(new Exception()));
+
+        webTestClient.delete()
+                .uri("/itachallenge/api/v1/user/users/" + userId + "/bookmarks/" + challengeId)
+                .exchange()
+                .expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)
+                .expectHeader().valueEquals("X-Bookmark-Deleted", "False")
+                .expectHeader().valueEquals("X-Bookmark-Message", "Unexpected server error.")
+                .expectBody(Boolean.class).isEqualTo(false);
+
+        verify(userService, times(1)).deleteChallengeFromBookmarks(userId, challengeId);
+    }
+
     @Test
     @DisplayName("GET /users/{userId}/favorites returns favorite challenges")
     void getUserFavorites_returnsFavorites() {
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 660939fb6..3ac93a4ba 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
@@ -385,6 +385,24 @@ void deleteChallengeFromFavorites_ShouldReturnFalse_WhenFavoritesIsNull() {
         verify(userRepository, times(0)).save(user);
     }
 
+    @Test
+    void deleteChallengeFromBookmarks_ShouldReturnFalse_WhenBookmarksIsNull() {
+        UUID challengeId = UUID.randomUUID();
+        UUID userId = UUID.randomUUID();
+        UserDocument user = new UserDocument(userId, "testUser", null, null, null);
+
+        when(userRepository.findById(userId)).thenReturn(Mono.just(user));
+
+        StepVerifier.create(userService.deleteChallengeFromBookmarks(userId.toString(), challengeId.toString()))
+                .expectNext(false)
+                .verifyComplete();
+
+        assertNull(user.getBookmarkChallenges());
+
+        verify(userRepository, times(1)).findById(userId);
+        verify(userRepository, times(0)).save(user);
+    }
+
     @Test
     void deleteChallengeFromFavorites_ShouldReturnFalse_WhenFavoritesIsEmpty() {
         UUID challengeId = UUID.randomUUID();
@@ -404,6 +422,25 @@ void deleteChallengeFromFavorites_ShouldReturnFalse_WhenFavoritesIsEmpty() {
         verify(userRepository, times(0)).save(user);
     }
 
+    @Test
+    void deleteChallengeFromBookmarks_ShouldReturnFalse_WhenBookmarksIsEmpty() {
+        UUID challengeId = UUID.randomUUID();
+        UUID userId = UUID.randomUUID();
+        UserDocument user = new UserDocument(userId, "testUser", null, null, new HashSet<>());
+
+        when(userRepository.findById(userId)).thenReturn(Mono.just(user));
+
+        StepVerifier.create(userService.deleteChallengeFromBookmarks(userId.toString(), challengeId.toString()))
+                .expectNext(false)
+                .verifyComplete();
+
+        assertNotNull(user.getBookmarkChallenges());
+        assertFalse(user.getBookmarkChallenges().contains(challengeId));
+
+        verify(userRepository, times(1)).findById(userId);
+        verify(userRepository, times(0)).save(user);
+    }
+
     @Test
     void deleteChallengeFromFavorites_ShouldReturnFalse_WhenChallengeNotInFavorites() {
         UUID challengeId = UUID.randomUUID();
@@ -424,6 +461,26 @@ void deleteChallengeFromFavorites_ShouldReturnFalse_WhenChallengeNotInFavorites(
         verify(userRepository, times(0)).save(user);
     }
 
+    @Test
+    void deleteChallengeFromBookmarks_ShouldReturnFalse_WhenChallengeNotInBookmarks() {
+        UUID challengeId = UUID.randomUUID();
+        UUID userId = UUID.randomUUID();
+        Set<UUID> bookmarks = new HashSet<>(Set.of(UUID.randomUUID(), UUID.randomUUID()));
+        UserDocument user = new UserDocument(userId, "testUser", null, null, bookmarks);
+
+        when(userRepository.findById(userId)).thenReturn(Mono.just(user));
+
+        StepVerifier.create(userService.deleteChallengeFromBookmarks(userId.toString(), challengeId.toString()))
+                .expectNext(false)
+                .verifyComplete();
+
+        assertNotNull(user.getBookmarkChallenges());
+        assertFalse(user.getBookmarkChallenges().contains(challengeId));
+
+        verify(userRepository, times(1)).findById(userId);
+        verify(userRepository, times(0)).save(user);
+    }
+
     @Test
     void deleteChallengeFromFavorites_ShouldReturnTrue_WhenFavoritesAlreadyContainsChallenge() {
         UUID challengeId = UUID.randomUUID();
@@ -445,6 +502,27 @@ void deleteChallengeFromFavorites_ShouldReturnTrue_WhenFavoritesAlreadyContainsC
         verify(userRepository, times(1)).save(any());
     }
 
+    @Test
+    void deleteChallengeFromBookmarks_ShouldReturnTrue_WhenBookmarksAlreadyContainsChallenge() {
+        UUID challengeId = UUID.randomUUID();
+        UUID userId = UUID.randomUUID();
+        Set<UUID> bookmarks = new HashSet<>(Set.of(UUID.randomUUID(), UUID.randomUUID(), challengeId));
+        UserDocument user = new UserDocument(userId, "testUser", null, null, bookmarks);
+
+        when(userRepository.findById(userId)).thenReturn(Mono.just(user));
+        when(userRepository.save(user)).thenReturn(Mono.just(user));
+
+        StepVerifier.create(userService.deleteChallengeFromBookmarks(userId.toString(), challengeId.toString()))
+                .expectNext(true)
+                .verifyComplete();
+
+        assertNotNull(user.getBookmarkChallenges());
+        assertFalse(user.getBookmarkChallenges().contains(challengeId));
+
+        verify(userRepository, times(1)).findById(userId);
+        verify(userRepository, times(1)).save(any());
+    }
+
     @Test
     void deleteChallengeFromFavorites_ShouldThrowNotFoundException_WhenUserNotFound() {
 
@@ -461,6 +539,22 @@ void deleteChallengeFromFavorites_ShouldThrowNotFoundException_WhenUserNotFound(
         verify(userRepository, times(0)).save(any());
     }
 
+    @Test
+    void deleteChallengeFromBookmarks_ShouldThrowNotFoundException_WhenUserNotFound() {
+
+        when(userRepository.findById(any(UUID.class))).thenReturn(Mono.empty());
+
+        StepVerifier.create(userService.deleteChallengeFromBookmarks(UUID.randomUUID().toString(), UUID.randomUUID().toString()))
+                .expectErrorSatisfies(throwable -> {
+                    assertInstanceOf(NotFoundException.class, throwable);
+                    assertEquals("User not found", throwable.getMessage());
+                })
+                .verify();
+
+        verify(userRepository, times(1)).findById(any(UUID.class));
+        verify(userRepository, times(0)).save(any());
+    }
+
     @Test
     void deleteChallengeFromFavorites_ShouldThrowBadRequestException_WhenUserUuidIsNull() {
         StepVerifier.create(userService.deleteChallengeFromFavorites(null, UUID.randomUUID().toString()))
@@ -474,6 +568,19 @@ void deleteChallengeFromFavorites_ShouldThrowBadRequestException_WhenUserUuidIsN
         verify(userRepository, times(0)).save(any());
     }
 
+    @Test
+    void deleteChallengeFromBookmarks_ShouldThrowBadRequestException_WhenUserUuidIsNull() {
+        StepVerifier.create(userService.deleteChallengeFromBookmarks(null, UUID.randomUUID().toString()))
+                .expectErrorSatisfies(throwable -> {
+                    assertInstanceOf(BadUUIDException.class, throwable);
+                    assertEquals("Invalid ID format", throwable.getMessage());
+                })
+                .verify();
+
+        verify(userRepository, times(0)).findById(any(UUID.class));
+        verify(userRepository, times(0)).save(any());
+    }
+
     @Test
     void deleteChallengeFromFavorites_ShouldThrowBadRequestException_WhenChallengeUuidIsNull() {
         StepVerifier.create(userService.deleteChallengeFromFavorites(UUID.randomUUID().toString(), null))
@@ -487,6 +594,19 @@ void deleteChallengeFromFavorites_ShouldThrowBadRequestException_WhenChallengeUu
         verify(userRepository, times(0)).save(any());
     }
 
+    @Test
+    void deleteChallengeFromBookmarks_ShouldThrowBadRequestException_WhenChallengeUuidIsNull() {
+        StepVerifier.create(userService.deleteChallengeFromBookmarks(UUID.randomUUID().toString(), null))
+                .expectErrorSatisfies(throwable -> {
+                    assertInstanceOf(BadUUIDException.class, throwable);
+                    assertEquals("Invalid ID format", throwable.getMessage());
+                })
+                .verify();
+
+        verify(userRepository, times(0)).findById(any(UUID.class));
+        verify(userRepository, times(0)).save(any());
+    }
+
     @Test
     void deleteChallengeFromFavorites_ShouldThrowBadRequestException_WhenUserUuidIsNotValid() {
         StepVerifier.create(userService.deleteChallengeFromFavorites("invalidUuid", UUID.randomUUID().toString()))
@@ -500,6 +620,19 @@ void deleteChallengeFromFavorites_ShouldThrowBadRequestException_WhenUserUuidIsN
         verify(userRepository, times(0)).save(any());
     }
 
+    @Test
+    void deleteChallengeFromBookmarks_ShouldThrowBadRequestException_WhenUserUuidIsNotValid() {
+        StepVerifier.create(userService.deleteChallengeFromBookmarks("invalidUuid", UUID.randomUUID().toString()))
+                .expectErrorSatisfies(throwable -> {
+                    assertInstanceOf(BadUUIDException.class, throwable);
+                    assertEquals("Invalid ID format", throwable.getMessage());
+                })
+                .verify();
+
+        verify(userRepository, times(0)).findById(any(UUID.class));
+        verify(userRepository, times(0)).save(any());
+    }
+
     @Test
     void deleteChallengeFromFavorites_ShouldThrowBadRequestException_WhenChallengeUuidIsNotValid() {
         StepVerifier.create(userService.deleteChallengeFromFavorites(UUID.randomUUID().toString(), "invalidUuid"))
@@ -513,6 +646,19 @@ void deleteChallengeFromFavorites_ShouldThrowBadRequestException_WhenChallengeUu
         verify(userRepository, times(0)).save(any());
     }
 
+    @Test
+    void deleteChallengeFromBookmarks_ShouldThrowBadRequestException_WhenChallengeUuidIsNotValid() {
+        StepVerifier.create(userService.deleteChallengeFromBookmarks(UUID.randomUUID().toString(), "invalidUuid"))
+                .expectErrorSatisfies(throwable -> {
+                    assertInstanceOf(BadUUIDException.class, throwable);
+                    assertEquals("Invalid ID format", throwable.getMessage());
+                })
+                .verify();
+
+        verify(userRepository, times(0)).findById(any(UUID.class));
+        verify(userRepository, times(0)).save(any());
+    }
+
     @Test
     @DisplayName("getUserFavorites returns favorite challenges when the user exists and has challenges")
     void getUserFavorites_WhenUserExistsWithFavorites_ReturnsSet() {


Reviews:
user: login: paumaneja
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-04T14:29:13Z
user: login: ivilarop
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T10:08:57Z
user: login: AlexandraBonetCanela
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T10:19:31Z
user: login: AlexandraBonetCanela
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T10:37:15Z
user: login: ivilarop
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-07T11:32:02Z

Review comments:
diff_hunk: @@ -332,12 +333,7 @@ public Mono<ResponseEntity<Boolean>> addToBookmarks(@PathVariable String userId,
             responses = {
                     @ApiResponse(
                             responseCode = "200",
-                            description = "No change, challenge was not in favorites",
path: itachallenge-user/src/main/java/com/itachallenge/user/controller/UserController.java
user: login: ivilarop
body: The change to the favorites endpoint (favorite DELETE endpoint) should have been addressed in a separate PR. It's technical debt.
created_at: 2025-04-07T09:54:40Z
author_association: COLLABORATOR
diff_hunk: @@ -15,8 +15,8 @@ public interface UserService {
 
     Mono<Boolean> deleteChallengeFromFavorites(String userId, String challengeId);
 
-    Mono<Set<UUID>> getUserFavorites(String userId);
-
+    Mono<Boolean> deleteChallengeFromBookmarks(String userId, String challengeId);
 
+    Mono<Set<UUID>> getUserFavorites(String userId);
path: itachallenge-user/src/main/java/com/itachallenge/user/service/UserService.java
user: login: ivilarop
body: be careful! getUserFavorites already existed and seems to have been moved without any functional changes. While the code is still correct, unnecessarily moving methods can make it harder to track history  and adds unnecessary noise to the PR.
created_at: 2025-04-07T10:03:18Z
author_association: COLLABORATOR
diff_hunk: @@ -332,12 +333,7 @@ public Mono<ResponseEntity<Boolean>> addToBookmarks(@PathVariable String userId,
             responses = {
                     @ApiResponse(
                             responseCode = "200",
-                            description = "No change, challenge was not in favorites",
path: itachallenge-user/src/main/java/com/itachallenge/user/controller/UserController.java
user: login: AlexandraBonetCanela
body: Noted!
created_at: 2025-04-07T10:19:31Z
author_association: COLLABORATOR
diff_hunk: @@ -15,8 +15,8 @@ public interface UserService {
 
     Mono<Boolean> deleteChallengeFromFavorites(String userId, String challengeId);
 
-    Mono<Set<UUID>> getUserFavorites(String userId);
-
+    Mono<Boolean> deleteChallengeFromBookmarks(String userId, String challengeId);
 
+    Mono<Set<UUID>> getUserFavorites(String userId);
path: itachallenge-user/src/main/java/com/itachallenge/user/service/UserService.java
user: login: AlexandraBonetCanela
body: Applied changes
created_at: 2025-04-07T10:37:15Z
author_association: COLLABORATOR

PR comments:
user: login: paumaneja
created_at: 2025-04-04T14:28:38Z
body: I have no specific comment.
Clearer PR than the previous one and following the thread of what was previously implemented in this microservice
author_association: COLLABORATOR
user: login: sonarqubecloud[bot]
created_at: 2025-04-07T10:42:22Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=IT-Academy-BCN_ita-challenges-backend&pullRequest=831) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [6 New issues](https://sonarcloud.io/project/issues?id=IT-Academy-BCN_ita-challenges-backend&pullRequest=831&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=IT-Academy-BCN_ita-challenges-backend&pullRequest=831&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=IT-Academy-BCN_ita-challenges-backend&pullRequest=831&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=IT-Academy-BCN_ita-challenges-backend&pullRequest=831&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=IT-Academy-BCN_ita-challenges-backend&pullRequest=831&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=IT-Academy-BCN_ita-challenges-backend&pullRequest=831)


author_association: NONE