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..afe046102 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,37 @@ 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) { + 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.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 new file mode 100644 index 000000000..e2eef2a4c --- /dev/null +++ b/src/main/java/org/patinanetwork/codebloom/api/admin/body/EditLeaderboardBody.java @@ -0,0 +1,49 @@ +package org.patinanetwork.codebloom.api.admin.body; + +import com.google.common.base.Strings; +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 { + + private String name; + + private OffsetDateTime shouldExpireBy; + + private String syntaxHighlightingLanguage; + + public void validate() { + var leaderboardName = getName(); + var expire = getShouldExpireBy(); + + if (Strings.isNullOrEmpty(leaderboardName)) { + throw new ValidationException("Leaderboard name cannot be null or empty"); + } + + if (leaderboardName.length() == 1) { + throw new ValidationException("Leaderboard name cannot have only 1 character"); + } + + if (leaderboardName.length() > 512) { + throw new ValidationException("Leaderboard name cannot have more than 512 characters"); + } + + if (expire != null) { + 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"); + } + } + } +} 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..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)); @@ -746,6 +747,7 @@ public boolean updateLeaderboard(final Leaderboard leaderboard) { name = :name, "createdAt" = :createdAt, "deletedAt" = :deletedAt, + "shouldExpireBy" = :shouldExpireBy, "syntaxHighlightingLanguage" = :syntaxHighlightingLanguage WHERE id = :id """; @@ -755,6 +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(SHOULD_EXPIRE_BY, leaderboard.getShouldExpireBy().orElse(null)); stmt.setObject("id", UUID.fromString(leaderboard.getId())); stmt.setString( "syntaxHighlightingLanguage", 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); + } + } }