From a4c5e76d0de358734d79df08d3c62c34e78ddf2e Mon Sep 17 00:00:00 2001 From: haokingluo Date: Tue, 17 Mar 2026 19:41:25 -0400 Subject: [PATCH 1/4] 853: create /leaderboard/current endpoint --- .../codebloom/api/admin/AdminController.java | 38 ++++++++++---- .../api/admin/body/EditLeaderboardBody.java | 51 +++++++++++++++++++ .../leaderboard/LeaderboardSqlRepository.java | 2 + 3 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 src/main/java/org/patinanetwork/codebloom/api/admin/body/EditLeaderboardBody.java diff --git a/src/main/java/org/patinanetwork/codebloom/api/admin/AdminController.java b/src/main/java/org/patinanetwork/codebloom/api/admin/AdminController.java index 908019ec3..7943a58b6 100644 --- a/src/main/java/org/patinanetwork/codebloom/api/admin/AdminController.java +++ b/src/main/java/org/patinanetwork/codebloom/api/admin/AdminController.java @@ -12,10 +12,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; -import org.patinanetwork.codebloom.api.admin.body.CreateAnnouncementBody; -import org.patinanetwork.codebloom.api.admin.body.DeleteAnnouncementBody; -import org.patinanetwork.codebloom.api.admin.body.NewLeaderboardBody; -import org.patinanetwork.codebloom.api.admin.body.UpdateAdminBody; +import org.patinanetwork.codebloom.api.admin.body.*; import org.patinanetwork.codebloom.api.admin.body.jda.DeleteMessageBody; import org.patinanetwork.codebloom.common.components.DiscordClubManager; import org.patinanetwork.codebloom.common.components.LeaderboardManager; @@ -38,12 +35,7 @@ import org.patinanetwork.codebloom.common.time.StandardizedOffsetDateTime; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; @RestController @@ -314,4 +306,30 @@ public ResponseEntity> deleteDiscordMessage( return ResponseEntity.ok(ApiResponder.success("Discord Message successfully deleted", Empty.of())); } + + @PutMapping("/leaderboard/current") + public ResponseEntity> editCurrentLeaderboard( + @RequestBody final EditLeaderboardBody editLeaderboardBody, final HttpServletRequest request) { + protector.validateAdminSession(request); + editLeaderboardBody.validate(); + + Optional currentLeaderboard = leaderboardRepository.getRecentLeaderboardMetadata(); + currentLeaderboard.ifPresent(lb -> { + OffsetDateTime shouldExpireBy = + StandardizedOffsetDateTime.normalize(editLeaderboardBody.getShouldExpireBy()); + + Leaderboard updated = Leaderboard.builder() + .name(editLeaderboardBody.getName()) + .deletedAt(lb.getDeletedAt()) + .createdAt(lb.getCreatedAt()) + .shouldExpireBy(Optional.of(shouldExpireBy).map(d -> d.toLocalDateTime())) + .syntaxHighlightingLanguage(Optional.of(editLeaderboardBody.getSyntaxHighlightingLanguage())) + .id(lb.getId()) + .build(); + + leaderboardRepository.updateLeaderboard(updated); + }); + + return ResponseEntity.ok().body(ApiResponder.success("Leaderboard updated successfully", Empty.of())); + } } diff --git a/src/main/java/org/patinanetwork/codebloom/api/admin/body/EditLeaderboardBody.java b/src/main/java/org/patinanetwork/codebloom/api/admin/body/EditLeaderboardBody.java new file mode 100644 index 000000000..6838dbc8f --- /dev/null +++ b/src/main/java/org/patinanetwork/codebloom/api/admin/body/EditLeaderboardBody.java @@ -0,0 +1,51 @@ +package org.patinanetwork.codebloom.api.admin.body; + +import com.google.common.base.Strings; +import jakarta.validation.constraints.NotBlank; +import java.time.OffsetDateTime; +import lombok.*; +import org.patinanetwork.codebloom.common.time.StandardizedOffsetDateTime; +import org.patinanetwork.codebloom.utilities.exception.ValidationException; + +@Getter +@Builder +@AllArgsConstructor +@ToString +public class EditLeaderboardBody { + + @NotBlank + private String name; + + private OffsetDateTime shouldExpireBy; + + private String syntaxHighlightingLanguage; + + public void validate() { + var name = getName(); + var expire = getShouldExpireBy(); + var highlight = getSyntaxHighlightingLanguage(); + + if (Strings.isNullOrEmpty(name)) { + throw new ValidationException("Leaderboard name cannot be null or empty"); + } + + if (name.length() == 1) { + throw new ValidationException("Leaderboard name cannot have only 1 character"); + } + + if (name.length() > 512) { + throw new ValidationException("Leaderboard name cannot have more than 512 characters"); + } + + OffsetDateTime nowWithOffset = StandardizedOffsetDateTime.now(); + OffsetDateTime expiresAtWithOffset = StandardizedOffsetDateTime.normalize(expire); + boolean isInFuture = nowWithOffset.isBefore(expiresAtWithOffset); + if (!isInFuture) { + throw new ValidationException("The expiration date must be in the future"); + } + + if (Strings.isNullOrEmpty(highlight)) { + throw new ValidationException("Syntax highlight language cannot be null or empty"); + } + } +} diff --git a/src/main/java/org/patinanetwork/codebloom/common/db/repos/leaderboard/LeaderboardSqlRepository.java b/src/main/java/org/patinanetwork/codebloom/common/db/repos/leaderboard/LeaderboardSqlRepository.java index 635e9b507..8ba9a491d 100644 --- a/src/main/java/org/patinanetwork/codebloom/common/db/repos/leaderboard/LeaderboardSqlRepository.java +++ b/src/main/java/org/patinanetwork/codebloom/common/db/repos/leaderboard/LeaderboardSqlRepository.java @@ -746,6 +746,7 @@ public boolean updateLeaderboard(final Leaderboard leaderboard) { name = :name, "createdAt" = :createdAt, "deletedAt" = :deletedAt, + "shouldExpireBy" = :shouldExpireBy, "syntaxHighlightingLanguage" = :syntaxHighlightingLanguage WHERE id = :id """; @@ -755,6 +756,7 @@ public boolean updateLeaderboard(final Leaderboard leaderboard) { stmt.setString("name", leaderboard.getName()); stmt.setObject("createdAt", leaderboard.getCreatedAt()); stmt.setObject("deletedAt", leaderboard.getDeletedAt().orElse(null)); + stmt.setObject("shouldExpireBy", leaderboard.getShouldExpireBy().orElse(null)); stmt.setObject("id", UUID.fromString(leaderboard.getId())); stmt.setString( "syntaxHighlightingLanguage", From bd07e5802e4f2f0f833c2bb63b1d57cf696de072 Mon Sep 17 00:00:00 2001 From: haokingluo Date: Wed, 18 Mar 2026 19:49:28 -0400 Subject: [PATCH 2/4] 853: add tests --- .../codebloom/api/admin/AdminController.java | 11 +- .../api/admin/body/EditLeaderboardBody.java | 28 +++-- .../api/admin/AdminControllerTest.java | 105 ++++++++++++++++++ 3 files changed, 127 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/patinanetwork/codebloom/api/admin/AdminController.java b/src/main/java/org/patinanetwork/codebloom/api/admin/AdminController.java index 7943a58b6..afe046102 100644 --- a/src/main/java/org/patinanetwork/codebloom/api/admin/AdminController.java +++ b/src/main/java/org/patinanetwork/codebloom/api/admin/AdminController.java @@ -307,6 +307,7 @@ public ResponseEntity> deleteDiscordMessage( return ResponseEntity.ok(ApiResponder.success("Discord Message successfully deleted", Empty.of())); } + @Operation(summary = "Edit current leaderboard") @PutMapping("/leaderboard/current") public ResponseEntity> editCurrentLeaderboard( @RequestBody final EditLeaderboardBody editLeaderboardBody, final HttpServletRequest request) { @@ -322,14 +323,20 @@ public ResponseEntity> editCurrentLeaderboard( .name(editLeaderboardBody.getName()) .deletedAt(lb.getDeletedAt()) .createdAt(lb.getCreatedAt()) - .shouldExpireBy(Optional.of(shouldExpireBy).map(d -> d.toLocalDateTime())) - .syntaxHighlightingLanguage(Optional.of(editLeaderboardBody.getSyntaxHighlightingLanguage())) + .shouldExpireBy(Optional.ofNullable(shouldExpireBy).map(d -> d.toLocalDateTime())) + .syntaxHighlightingLanguage( + Optional.ofNullable(editLeaderboardBody.getSyntaxHighlightingLanguage())) .id(lb.getId()) .build(); leaderboardRepository.updateLeaderboard(updated); }); + if (currentLeaderboard.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(ApiResponder.failure("No current leaderboard found")); + } + return ResponseEntity.ok().body(ApiResponder.success("Leaderboard updated successfully", Empty.of())); } } diff --git a/src/main/java/org/patinanetwork/codebloom/api/admin/body/EditLeaderboardBody.java b/src/main/java/org/patinanetwork/codebloom/api/admin/body/EditLeaderboardBody.java index 6838dbc8f..e2eef2a4c 100644 --- a/src/main/java/org/patinanetwork/codebloom/api/admin/body/EditLeaderboardBody.java +++ b/src/main/java/org/patinanetwork/codebloom/api/admin/body/EditLeaderboardBody.java @@ -1,19 +1,19 @@ package org.patinanetwork.codebloom.api.admin.body; import com.google.common.base.Strings; -import jakarta.validation.constraints.NotBlank; import java.time.OffsetDateTime; import lombok.*; +import lombok.extern.jackson.Jacksonized; import org.patinanetwork.codebloom.common.time.StandardizedOffsetDateTime; import org.patinanetwork.codebloom.utilities.exception.ValidationException; @Getter @Builder +@Jacksonized @AllArgsConstructor @ToString public class EditLeaderboardBody { - @NotBlank private String name; private OffsetDateTime shouldExpireBy; @@ -21,31 +21,29 @@ public class EditLeaderboardBody { private String syntaxHighlightingLanguage; public void validate() { - var name = getName(); + var leaderboardName = getName(); var expire = getShouldExpireBy(); - var highlight = getSyntaxHighlightingLanguage(); - if (Strings.isNullOrEmpty(name)) { + if (Strings.isNullOrEmpty(leaderboardName)) { throw new ValidationException("Leaderboard name cannot be null or empty"); } - if (name.length() == 1) { + if (leaderboardName.length() == 1) { throw new ValidationException("Leaderboard name cannot have only 1 character"); } - if (name.length() > 512) { + if (leaderboardName.length() > 512) { throw new ValidationException("Leaderboard name cannot have more than 512 characters"); } - OffsetDateTime nowWithOffset = StandardizedOffsetDateTime.now(); - OffsetDateTime expiresAtWithOffset = StandardizedOffsetDateTime.normalize(expire); - boolean isInFuture = nowWithOffset.isBefore(expiresAtWithOffset); - if (!isInFuture) { - throw new ValidationException("The expiration date must be in the future"); - } + if (expire != null) { + OffsetDateTime nowWithOffset = StandardizedOffsetDateTime.now(); + OffsetDateTime expiresAtWithOffset = StandardizedOffsetDateTime.normalize(expire); + boolean isInFuture = nowWithOffset.isBefore(expiresAtWithOffset); - if (Strings.isNullOrEmpty(highlight)) { - throw new ValidationException("Syntax highlight language cannot be null or empty"); + if (!isInFuture) { + throw new ValidationException("The expiration date must be in the future"); + } } } } diff --git a/src/test/java/org/patinanetwork/codebloom/api/admin/AdminControllerTest.java b/src/test/java/org/patinanetwork/codebloom/api/admin/AdminControllerTest.java index 172558992..d2e657857 100644 --- a/src/test/java/org/patinanetwork/codebloom/api/admin/AdminControllerTest.java +++ b/src/test/java/org/patinanetwork/codebloom/api/admin/AdminControllerTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.patinanetwork.codebloom.api.admin.body.DeleteAnnouncementBody; +import org.patinanetwork.codebloom.api.admin.body.EditLeaderboardBody; import org.patinanetwork.codebloom.api.admin.body.NewLeaderboardBody; import org.patinanetwork.codebloom.api.admin.body.jda.DeleteMessageBody; import org.patinanetwork.codebloom.common.components.DiscordClubManager; @@ -29,6 +30,8 @@ import org.patinanetwork.codebloom.common.dto.Empty; import org.patinanetwork.codebloom.common.dto.question.QuestionWithUserDto; import org.patinanetwork.codebloom.common.security.Protector; +import org.patinanetwork.codebloom.common.time.StandardizedOffsetDateTime; +import org.patinanetwork.codebloom.utilities.exception.ValidationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.server.ResponseStatusException; @@ -524,4 +527,106 @@ void testDeleteDiscordMessageSuccess() { verify(protector).validateAdminSession(request); } + + @Test + void testEditCurrentLeaderboardSuccess() { + OffsetDateTime date = StandardizedOffsetDateTime.normalize(OffsetDateTime.parse("4096-01-01T00:00:00Z")); + + EditLeaderboardBody body = EditLeaderboardBody.builder() + .name("std::string name = new lb") + .syntaxHighlightingLanguage("cpp") + .shouldExpireBy(date) + .build(); + + Leaderboard currentLeaderboard = + Leaderboard.builder().name("current leaderboard").id("123").build(); + when(leaderboardRepository.getRecentLeaderboardMetadata()).thenReturn(Optional.of(currentLeaderboard)); + + ResponseEntity> response = adminController.editCurrentLeaderboard(body, request); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + assertTrue(response.getBody().isSuccess()); + assertEquals("Leaderboard updated successfully", response.getBody().getMessage()); + + verify(protector).validateAdminSession(request); + } + + @Test + void testEditCurrentLeaderboardFailureNoCurrentLeaderboard() { + OffsetDateTime date = StandardizedOffsetDateTime.normalize(OffsetDateTime.parse("4096-01-01T00:00:00Z")); + + EditLeaderboardBody body = EditLeaderboardBody.builder() + .name("std::string name = new lb") + .syntaxHighlightingLanguage("cpp") + .shouldExpireBy(date) + .build(); + + when(leaderboardRepository.getRecentLeaderboardMetadata()).thenReturn(Optional.empty()); + + ResponseEntity> response = adminController.editCurrentLeaderboard(body, request); + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + assertNotNull(response.getBody()); + assertFalse(response.getBody().isSuccess()); + assertEquals("No current leaderboard found", response.getBody().getMessage()); + + verify(protector).validateAdminSession(request); + } + + @Test + void testEditCurrentLeaderboardFailureNameTooShort() { + OffsetDateTime date = StandardizedOffsetDateTime.normalize(OffsetDateTime.parse("4096-01-01T00:00:00Z")); + EditLeaderboardBody body = EditLeaderboardBody.builder() + .name("1") + .syntaxHighlightingLanguage("cpp") + .shouldExpireBy(date) + .build(); + + try { + adminController.editCurrentLeaderboard(body, request); + fail("Exception expected"); + } catch (ValidationException e) { + assertNotNull(e); + assertTrue(e.getMessage().contains("Leaderboard name cannot have only 1 character")); + assertInstanceOf(ValidationException.class, e); + } + } + + @Test + void testEditCurrentLeaderboardFailureNameTooLong() { + OffsetDateTime date = StandardizedOffsetDateTime.normalize(OffsetDateTime.parse("4096-01-01T00:00:00Z")); + String longName = "a".repeat(513); + EditLeaderboardBody body = EditLeaderboardBody.builder() + .name(longName) + .syntaxHighlightingLanguage("cpp") + .shouldExpireBy(date) + .build(); + + try { + adminController.editCurrentLeaderboard(body, request); + fail("Exception expected"); + } catch (ValidationException e) { + assertNotNull(e); + assertTrue(e.getMessage().contains("Leaderboard name cannot have more than 512 characters")); + assertInstanceOf(ValidationException.class, e); + } + } + + @Test + void testEditCurrentLeaderboardFailurePastDate() { + OffsetDateTime date = StandardizedOffsetDateTime.normalize(OffsetDateTime.parse("2000-01-01T00:00:00Z")); + EditLeaderboardBody body = EditLeaderboardBody.builder() + .name("new lb name") + .syntaxHighlightingLanguage("cpp") + .shouldExpireBy(date) + .build(); + + try { + adminController.editCurrentLeaderboard(body, request); + fail("Exception expected"); + } catch (ValidationException e) { + assertNotNull(e); + assertTrue(e.getMessage().contains("The expiration date must be in the future")); + assertInstanceOf(ValidationException.class, e); + } + } } From f70860ace13c92f6cdebe9174c656fd2201f6462 Mon Sep 17 00:00:00 2001 From: haokingluo Date: Thu, 19 Mar 2026 15:58:00 -0400 Subject: [PATCH 3/4] 853: sonarqube fix --- .../db/repos/leaderboard/LeaderboardSqlRepository.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/patinanetwork/codebloom/common/db/repos/leaderboard/LeaderboardSqlRepository.java b/src/main/java/org/patinanetwork/codebloom/common/db/repos/leaderboard/LeaderboardSqlRepository.java index 8ba9a491d..be77bc651 100644 --- a/src/main/java/org/patinanetwork/codebloom/common/db/repos/leaderboard/LeaderboardSqlRepository.java +++ b/src/main/java/org/patinanetwork/codebloom/common/db/repos/leaderboard/LeaderboardSqlRepository.java @@ -29,6 +29,7 @@ public class LeaderboardSqlRepository implements LeaderboardRepository { private DataSource ds; private final UserRepository userRepository; + private static final String SHOULD_EXPIRE_BY = "shouldExpireBy"; public LeaderboardSqlRepository(final DataSource ds, final UserRepository userRepository) { this.ds = ds; @@ -42,7 +43,7 @@ private Leaderboard parseResultSetToLeaderboard(final ResultSet resultSet) throw .deletedAt( Optional.ofNullable(resultSet.getTimestamp("deletedAt")).map(Timestamp::toLocalDateTime)) .name(resultSet.getString("name")) - .shouldExpireBy(Optional.ofNullable(resultSet.getTimestamp("shouldExpireBy")) + .shouldExpireBy(Optional.ofNullable(resultSet.getTimestamp(SHOULD_EXPIRE_BY)) .map(Timestamp::toLocalDateTime)) .syntaxHighlightingLanguage(Optional.ofNullable(resultSet.getString("syntaxHighlightingLanguage"))) .build(); @@ -86,7 +87,7 @@ public void addNewLeaderboard(final Leaderboard leaderboard) { NamedPreparedStatement stmt = new NamedPreparedStatement(conn, sql)) { stmt.setObject("id", UUID.fromString(leaderboard.getId())); stmt.setString("name", leaderboard.getName()); - stmt.setObject("shouldExpireBy", leaderboard.getShouldExpireBy().orElse(null)); + stmt.setObject(SHOULD_EXPIRE_BY, leaderboard.getShouldExpireBy().orElse(null)); stmt.setString( "syntaxHighlightingLanguage", leaderboard.getSyntaxHighlightingLanguage().orElse(null)); @@ -756,7 +757,7 @@ public boolean updateLeaderboard(final Leaderboard leaderboard) { stmt.setString("name", leaderboard.getName()); stmt.setObject("createdAt", leaderboard.getCreatedAt()); stmt.setObject("deletedAt", leaderboard.getDeletedAt().orElse(null)); - stmt.setObject("shouldExpireBy", leaderboard.getShouldExpireBy().orElse(null)); + stmt.setObject(SHOULD_EXPIRE_BY, leaderboard.getShouldExpireBy().orElse(null)); stmt.setObject("id", UUID.fromString(leaderboard.getId())); stmt.setString( "syntaxHighlightingLanguage", From 30227b2a0eaf02f6b81fd81948a9f74d7ec40dcc Mon Sep 17 00:00:00 2001 From: haokingluo Date: Sat, 21 Mar 2026 14:22:49 -0400 Subject: [PATCH 4/4] 853: throw instead of 404 --- .../codebloom/api/admin/AdminController.java | 3 +-- .../codebloom/api/admin/AdminControllerTest.java | 14 +++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/patinanetwork/codebloom/api/admin/AdminController.java b/src/main/java/org/patinanetwork/codebloom/api/admin/AdminController.java index afe046102..26857d0db 100644 --- a/src/main/java/org/patinanetwork/codebloom/api/admin/AdminController.java +++ b/src/main/java/org/patinanetwork/codebloom/api/admin/AdminController.java @@ -333,8 +333,7 @@ public ResponseEntity> editCurrentLeaderboard( }); if (currentLeaderboard.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponder.failure("No current leaderboard found")); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No current leaderboard found"); } return ResponseEntity.ok().body(ApiResponder.success("Leaderboard updated successfully", Empty.of())); diff --git a/src/test/java/org/patinanetwork/codebloom/api/admin/AdminControllerTest.java b/src/test/java/org/patinanetwork/codebloom/api/admin/AdminControllerTest.java index d2e657857..4a9070b06 100644 --- a/src/test/java/org/patinanetwork/codebloom/api/admin/AdminControllerTest.java +++ b/src/test/java/org/patinanetwork/codebloom/api/admin/AdminControllerTest.java @@ -563,13 +563,13 @@ void testEditCurrentLeaderboardFailureNoCurrentLeaderboard() { when(leaderboardRepository.getRecentLeaderboardMetadata()).thenReturn(Optional.empty()); - ResponseEntity> response = adminController.editCurrentLeaderboard(body, request); - assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); - assertNotNull(response.getBody()); - assertFalse(response.getBody().isSuccess()); - assertEquals("No current leaderboard found", response.getBody().getMessage()); - - verify(protector).validateAdminSession(request); + try { + adminController.editCurrentLeaderboard(body, request); + fail("Exception expected"); + } catch (ResponseStatusException e) { + assertNotNull(e); + assertTrue(e.getMessage().contains("No current leaderboard found")); + } } @Test