From b05e9aae498fabb90437c2f2b7af4e40dff73cfa Mon Sep 17 00:00:00 2001 From: Angela Yu Date: Thu, 6 Nov 2025 18:14:02 -0500 Subject: [PATCH 01/12] wrote create party endpoint + wrote a random code generator --- .../codebloom/api/duel/DuelController.java | 62 +++++++++++++++++-- .../common/util/PartyCodeGenerator.java | 34 ++++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/patina/codebloom/common/util/PartyCodeGenerator.java diff --git a/src/main/java/com/patina/codebloom/api/duel/DuelController.java b/src/main/java/com/patina/codebloom/api/duel/DuelController.java index eafc9f29c..2a04a1f46 100644 --- a/src/main/java/com/patina/codebloom/api/duel/DuelController.java +++ b/src/main/java/com/patina/codebloom/api/duel/DuelController.java @@ -1,5 +1,7 @@ package com.patina.codebloom.api.duel; +import java.time.OffsetDateTime; + import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -8,13 +10,23 @@ import org.springframework.web.server.ResponseStatusException; import com.patina.codebloom.common.components.DuelManager; +import com.patina.codebloom.common.db.models.lobby.Lobby; +import com.patina.codebloom.common.db.models.lobby.LobbyStatus; +import com.patina.codebloom.common.db.models.lobby.player.LobbyPlayer; +import com.patina.codebloom.common.db.repos.lobby.LobbyRepository; +import com.patina.codebloom.common.db.repos.lobby.player.LobbyPlayerRepository; import com.patina.codebloom.common.dto.ApiResponder; import com.patina.codebloom.common.dto.Empty; import com.patina.codebloom.common.env.Env; +import com.patina.codebloom.common.security.AuthenticationObject; +import com.patina.codebloom.common.security.Protector; +import com.patina.codebloom.common.time.StandardizedOffsetDateTime; +import com.patina.codebloom.common.util.PartyCodeGenerator; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; @RestController @Tag(name = "Live duel routes", description = """ @@ -24,10 +36,17 @@ public class DuelController { private final Env env; private final DuelManager duelManager; + private final LobbyRepository lobbyRepository; + private final LobbyPlayerRepository lobbyPlayerRepository; + private final Protector protector; - public DuelController(final Env env, final DuelManager duelManager) { + public DuelController(final Env env, final DuelManager duelManager, final LobbyRepository lobbyRepository, + final LobbyPlayerRepository lobbyPlayerRepository, final Protector protector) { this.env = env; this.duelManager = duelManager; + this.lobbyRepository = lobbyRepository; + this.lobbyPlayerRepository = lobbyPlayerRepository; + this.protector = protector; } @Operation(summary = "Join party", description = "WIP") @@ -52,14 +71,47 @@ public ResponseEntity> leaveParty() { return ResponseEntity.ok(ApiResponder.success("ok", Empty.of())); } - @Operation(summary = "Create party", description = "WIP") - @ApiResponse(responseCode = "403", description = "Endpoint is currently non-functional") + @Operation(summary = "Create party", description = "Create a new lobby and become the host") + @ApiResponse(responseCode = "200", description = "Lobby created successfully") + @ApiResponse(responseCode = "400", description = "Player is already in a lobby") + @ApiResponse(responseCode = "401", description = "User not authenticated") @PostMapping("/party/create") - public ResponseEntity> createParty() { + public ResponseEntity> createParty(final HttpServletRequest request) { if (env.isProd()) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Endpoint is currently non-functional"); } - return ResponseEntity.ok(ApiResponder.success("ok", Empty.of())); + AuthenticationObject authObject = protector.validateSession(request); + String playerId = authObject.getUser().getId(); + + LobbyPlayer existingLobbyPlayer = lobbyPlayerRepository.findLobbyPlayerByPlayerId(playerId); + if (existingLobbyPlayer != null) { + return ResponseEntity.badRequest() + .body(ApiResponder.failure("You are already in a lobby. Leave your current lobby first.")); + } + + String joinCode = PartyCodeGenerator.generateUniqueCode(code -> lobbyRepository.findLobbyByJoinCode(code) != null); + + OffsetDateTime expiresAt = StandardizedOffsetDateTime.now().plusMinutes(30); + + Lobby lobby = Lobby.builder() + .joinCode(joinCode) + .status(LobbyStatus.AVAILABLE) + .expiresAt(expiresAt) + .playerCount(1) + .winnerId(null) + .build(); + + lobbyRepository.createLobby(lobby); + + LobbyPlayer lobbyPlayer = LobbyPlayer.builder() + .lobbyId(lobby.getId()) + .playerId(playerId) + .points(0) + .build(); + + lobbyPlayerRepository.createLobbyPlayer(lobbyPlayer); + + return ResponseEntity.ok(ApiResponder.success("Lobby created successfully! Share the join code: " + lobby.getJoinCode(), Empty.of())); } } diff --git a/src/main/java/com/patina/codebloom/common/util/PartyCodeGenerator.java b/src/main/java/com/patina/codebloom/common/util/PartyCodeGenerator.java new file mode 100644 index 000000000..bde6b3106 --- /dev/null +++ b/src/main/java/com/patina/codebloom/common/util/PartyCodeGenerator.java @@ -0,0 +1,34 @@ +package com.patina.codebloom.common.util; + +import java.security.SecureRandom; + + +public class PartyCodeGenerator { + private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + private static final int CODE_LENGTH = 6; + private static final SecureRandom RANDOM = new SecureRandom(); + + /** + * Generates a unique party code by checking against existing codes. + * + * @param existingCodeChecker Function to check if a code already exists + * @return A unique party code (e.g., "ABC123") + */ + public static String generateUniqueCode(final java.util.function.Predicate existingCodeChecker) { + final int maxAttempts = 10; + + for (int attempts = 0; attempts < maxAttempts; attempts++) { + StringBuilder codeBuilder = new StringBuilder(CODE_LENGTH); + for (int i = 0; i < CODE_LENGTH; i++) { + codeBuilder.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length()))); + } + String code = codeBuilder.toString(); + + if (!existingCodeChecker.test(code)) { + return code; + } + } + + throw new RuntimeException("Failed to generate unique party code after " + maxAttempts + " attempts"); + } +} \ No newline at end of file From 5716d9e0593690d2863596d3e3aba7147894c701 Mon Sep 17 00:00:00 2001 From: Angela Yu Date: Thu, 6 Nov 2025 18:14:10 -0500 Subject: [PATCH 02/12] Lobby Dto --- .../codebloom/api/duel/dto/LobbyDto.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/main/java/com/patina/codebloom/api/duel/dto/LobbyDto.java diff --git a/src/main/java/com/patina/codebloom/api/duel/dto/LobbyDto.java b/src/main/java/com/patina/codebloom/api/duel/dto/LobbyDto.java new file mode 100644 index 000000000..468150399 --- /dev/null +++ b/src/main/java/com/patina/codebloom/api/duel/dto/LobbyDto.java @@ -0,0 +1,47 @@ +package com.patina.codebloom.api.duel.dto; + +import java.time.OffsetDateTime; + +import com.patina.codebloom.common.db.models.lobby.Lobby; +import com.patina.codebloom.common.db.models.lobby.LobbyStatus; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.jackson.Jacksonized; + +@Getter +@Builder +@Jacksonized +@ToString +@EqualsAndHashCode +public class LobbyDto { + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + private String id; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + private String joinCode; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + private LobbyStatus status; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime expiresAt; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + private OffsetDateTime createdAt; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + private int playerCount; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, nullable = true) + private String winnerId; + + public static LobbyDto fromLobby(final Lobby lobby) { + return LobbyDto.builder() + .id(lobby.getId()) + .joinCode(lobby.getJoinCode()) + .status(lobby.getStatus()) + .expiresAt(lobby.getExpiresAt()) + .createdAt(lobby.getCreatedAt()) + .playerCount(lobby.getPlayerCount()) + .winnerId(lobby.getWinnerId()) + .build(); + } +} \ No newline at end of file From 093a9c6a0d813a7675983eebd38737a4575d3f27 Mon Sep 17 00:00:00 2001 From: Angela Yu Date: Thu, 6 Nov 2025 18:35:11 -0500 Subject: [PATCH 03/12] co pilot pr changes --- .../patina/codebloom/common/util/PartyCodeGenerator.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/patina/codebloom/common/util/PartyCodeGenerator.java b/src/main/java/com/patina/codebloom/common/util/PartyCodeGenerator.java index bde6b3106..0079f8d17 100644 --- a/src/main/java/com/patina/codebloom/common/util/PartyCodeGenerator.java +++ b/src/main/java/com/patina/codebloom/common/util/PartyCodeGenerator.java @@ -2,10 +2,10 @@ import java.security.SecureRandom; - public class PartyCodeGenerator { private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; private static final int CODE_LENGTH = 6; + private static final int MAX_ATTEMPTS = 10; private static final SecureRandom RANDOM = new SecureRandom(); /** @@ -15,9 +15,8 @@ public class PartyCodeGenerator { * @return A unique party code (e.g., "ABC123") */ public static String generateUniqueCode(final java.util.function.Predicate existingCodeChecker) { - final int maxAttempts = 10; - for (int attempts = 0; attempts < maxAttempts; attempts++) { + for (int attempts = 0; attempts < MAX_ATTEMPTS; attempts++) { StringBuilder codeBuilder = new StringBuilder(CODE_LENGTH); for (int i = 0; i < CODE_LENGTH; i++) { codeBuilder.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length()))); @@ -29,6 +28,6 @@ public static String generateUniqueCode(final java.util.function.Predicate Date: Thu, 6 Nov 2025 18:52:54 -0500 Subject: [PATCH 04/12] other copiliot changes Update src/main/java/com/patina/codebloom/api/duel/DuelController.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> nits --- .../com/patina/codebloom/api/duel/DuelController.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/patina/codebloom/api/duel/DuelController.java b/src/main/java/com/patina/codebloom/api/duel/DuelController.java index 2a04a1f46..f37620890 100644 --- a/src/main/java/com/patina/codebloom/api/duel/DuelController.java +++ b/src/main/java/com/patina/codebloom/api/duel/DuelController.java @@ -1,6 +1,9 @@ package com.patina.codebloom.api.duel; +import java.sql.Connection; +import java.sql.SQLException; import java.time.OffsetDateTime; +import java.util.List; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -13,6 +16,7 @@ import com.patina.codebloom.common.db.models.lobby.Lobby; import com.patina.codebloom.common.db.models.lobby.LobbyStatus; import com.patina.codebloom.common.db.models.lobby.player.LobbyPlayer; +import com.patina.codebloom.common.db.models.user.User; import com.patina.codebloom.common.db.repos.lobby.LobbyRepository; import com.patina.codebloom.common.db.repos.lobby.player.LobbyPlayerRepository; import com.patina.codebloom.common.dto.ApiResponder; @@ -81,8 +85,9 @@ public ResponseEntity> createParty(final HttpServletRequest throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Endpoint is currently non-functional"); } - AuthenticationObject authObject = protector.validateSession(request); - String playerId = authObject.getUser().getId(); + AuthenticationObject authenticationObject = protector.validateSession(request); + User user = authenticationObject.getUser(); + String playerId = user.getId(); LobbyPlayer existingLobbyPlayer = lobbyPlayerRepository.findLobbyPlayerByPlayerId(playerId); if (existingLobbyPlayer != null) { From a8f7655da1d00b5b4667b75c6501935d6f5aab75 Mon Sep 17 00:00:00 2001 From: Angela Yu <146024318+angelayu0530@users.noreply.github.com> Date: Thu, 6 Nov 2025 20:30:32 -0500 Subject: [PATCH 05/12] Update src/main/java/com/patina/codebloom/api/duel/DuelController.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/main/java/com/patina/codebloom/api/duel/DuelController.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/patina/codebloom/api/duel/DuelController.java b/src/main/java/com/patina/codebloom/api/duel/DuelController.java index f37620890..162a05c88 100644 --- a/src/main/java/com/patina/codebloom/api/duel/DuelController.java +++ b/src/main/java/com/patina/codebloom/api/duel/DuelController.java @@ -1,7 +1,5 @@ package com.patina.codebloom.api.duel; -import java.sql.Connection; -import java.sql.SQLException; import java.time.OffsetDateTime; import java.util.List; From f928828a49bcce4bbf82bdb93b33096934c42eda Mon Sep 17 00:00:00 2001 From: Angela Yu Date: Thu, 6 Nov 2025 20:34:05 -0500 Subject: [PATCH 06/12] Checkstyles --- src/main/java/com/patina/codebloom/api/duel/DuelController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/patina/codebloom/api/duel/DuelController.java b/src/main/java/com/patina/codebloom/api/duel/DuelController.java index 162a05c88..ee148e9d8 100644 --- a/src/main/java/com/patina/codebloom/api/duel/DuelController.java +++ b/src/main/java/com/patina/codebloom/api/duel/DuelController.java @@ -1,7 +1,6 @@ package com.patina.codebloom.api.duel; import java.time.OffsetDateTime; -import java.util.List; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; From 4a759935706458304478d7b461b58fa1d8450a56 Mon Sep 17 00:00:00 2001 From: Angela Yu Date: Thu, 6 Nov 2025 21:29:07 -0500 Subject: [PATCH 07/12] added check for if a party is already in a lobby and fixed the protector nit nits --- .../patina/codebloom/api/duel/DuelController.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/patina/codebloom/api/duel/DuelController.java b/src/main/java/com/patina/codebloom/api/duel/DuelController.java index ee148e9d8..8300e4279 100644 --- a/src/main/java/com/patina/codebloom/api/duel/DuelController.java +++ b/src/main/java/com/patina/codebloom/api/duel/DuelController.java @@ -20,14 +20,13 @@ import com.patina.codebloom.common.dto.Empty; import com.patina.codebloom.common.env.Env; import com.patina.codebloom.common.security.AuthenticationObject; -import com.patina.codebloom.common.security.Protector; +import com.patina.codebloom.common.security.annotation.Protected; import com.patina.codebloom.common.time.StandardizedOffsetDateTime; import com.patina.codebloom.common.util.PartyCodeGenerator; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; @RestController @Tag(name = "Live duel routes", description = """ @@ -39,15 +38,13 @@ public class DuelController { private final DuelManager duelManager; private final LobbyRepository lobbyRepository; private final LobbyPlayerRepository lobbyPlayerRepository; - private final Protector protector; public DuelController(final Env env, final DuelManager duelManager, final LobbyRepository lobbyRepository, - final LobbyPlayerRepository lobbyPlayerRepository, final Protector protector) { + final LobbyPlayerRepository lobbyPlayerRepository) { this.env = env; this.duelManager = duelManager; this.lobbyRepository = lobbyRepository; this.lobbyPlayerRepository = lobbyPlayerRepository; - this.protector = protector; } @Operation(summary = "Join party", description = "WIP") @@ -76,20 +73,20 @@ public ResponseEntity> leaveParty() { @ApiResponse(responseCode = "200", description = "Lobby created successfully") @ApiResponse(responseCode = "400", description = "Player is already in a lobby") @ApiResponse(responseCode = "401", description = "User not authenticated") + @Protected @PostMapping("/party/create") - public ResponseEntity> createParty(final HttpServletRequest request) { + public ResponseEntity> createParty(final AuthenticationObject authenticationObject) { if (env.isProd()) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Endpoint is currently non-functional"); } - AuthenticationObject authenticationObject = protector.validateSession(request); User user = authenticationObject.getUser(); String playerId = user.getId(); LobbyPlayer existingLobbyPlayer = lobbyPlayerRepository.findLobbyPlayerByPlayerId(playerId); if (existingLobbyPlayer != null) { return ResponseEntity.badRequest() - .body(ApiResponder.failure("You are already in a lobby. Leave your current lobby first.")); + .body(ApiResponder.failure("You are already in a lobby. Please leave your current lobby before creating a new one.")); } String joinCode = PartyCodeGenerator.generateUniqueCode(code -> lobbyRepository.findLobbyByJoinCode(code) != null); From 5e51a294444b0dc510360428f6c15c4211b0379c Mon Sep 17 00:00:00 2001 From: Angela Yu Date: Fri, 7 Nov 2025 00:09:45 -0500 Subject: [PATCH 08/12] removing db collisons check --- .../codebloom/api/duel/DuelController.java | 2 +- .../common/util/PartyCodeGenerator.java | 25 ++++++------------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/patina/codebloom/api/duel/DuelController.java b/src/main/java/com/patina/codebloom/api/duel/DuelController.java index 8300e4279..e239d6609 100644 --- a/src/main/java/com/patina/codebloom/api/duel/DuelController.java +++ b/src/main/java/com/patina/codebloom/api/duel/DuelController.java @@ -89,7 +89,7 @@ public ResponseEntity> createParty(final AuthenticationObjec .body(ApiResponder.failure("You are already in a lobby. Please leave your current lobby before creating a new one.")); } - String joinCode = PartyCodeGenerator.generateUniqueCode(code -> lobbyRepository.findLobbyByJoinCode(code) != null); + String joinCode = PartyCodeGenerator.generateCode(); OffsetDateTime expiresAt = StandardizedOffsetDateTime.now().plusMinutes(30); diff --git a/src/main/java/com/patina/codebloom/common/util/PartyCodeGenerator.java b/src/main/java/com/patina/codebloom/common/util/PartyCodeGenerator.java index 0079f8d17..3bce6eb67 100644 --- a/src/main/java/com/patina/codebloom/common/util/PartyCodeGenerator.java +++ b/src/main/java/com/patina/codebloom/common/util/PartyCodeGenerator.java @@ -5,29 +5,18 @@ public class PartyCodeGenerator { private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; private static final int CODE_LENGTH = 6; - private static final int MAX_ATTEMPTS = 10; private static final SecureRandom RANDOM = new SecureRandom(); /** - * Generates a unique party code by checking against existing codes. + * Generates a random party code * - * @param existingCodeChecker Function to check if a code already exists - * @return A unique party code (e.g., "ABC123") + * @return A random party code (e.g., "ABC123") */ - public static String generateUniqueCode(final java.util.function.Predicate existingCodeChecker) { - - for (int attempts = 0; attempts < MAX_ATTEMPTS; attempts++) { - StringBuilder codeBuilder = new StringBuilder(CODE_LENGTH); - for (int i = 0; i < CODE_LENGTH; i++) { - codeBuilder.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length()))); - } - String code = codeBuilder.toString(); - - if (!existingCodeChecker.test(code)) { - return code; - } + public static String generateCode() { + StringBuilder codeBuilder = new StringBuilder(CODE_LENGTH); + for (int i = 0; i < CODE_LENGTH; i++) { + codeBuilder.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length()))); } - - throw new RuntimeException("Failed to generate unique party code after " + MAX_ATTEMPTS + " attempts"); + return codeBuilder.toString(); } } \ No newline at end of file From 9463830c2c1addaacb1708f19503f188965a117b Mon Sep 17 00:00:00 2001 From: Angela Yu Date: Fri, 7 Nov 2025 16:09:23 -0500 Subject: [PATCH 09/12] changes with protector --- .../java/com/patina/codebloom/api/duel/DuelController.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/patina/codebloom/api/duel/DuelController.java b/src/main/java/com/patina/codebloom/api/duel/DuelController.java index e239d6609..8ce2e77cf 100644 --- a/src/main/java/com/patina/codebloom/api/duel/DuelController.java +++ b/src/main/java/com/patina/codebloom/api/duel/DuelController.java @@ -73,9 +73,8 @@ public ResponseEntity> leaveParty() { @ApiResponse(responseCode = "200", description = "Lobby created successfully") @ApiResponse(responseCode = "400", description = "Player is already in a lobby") @ApiResponse(responseCode = "401", description = "User not authenticated") - @Protected @PostMapping("/party/create") - public ResponseEntity> createParty(final AuthenticationObject authenticationObject) { + public ResponseEntity> createParty( @Protected final AuthenticationObject authenticationObject) { if (env.isProd()) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Endpoint is currently non-functional"); } @@ -90,7 +89,6 @@ public ResponseEntity> createParty(final AuthenticationObjec } String joinCode = PartyCodeGenerator.generateCode(); - OffsetDateTime expiresAt = StandardizedOffsetDateTime.now().plusMinutes(30); Lobby lobby = Lobby.builder() From 1df446bc051536ed34d4d1dacf790770438a8997 Mon Sep 17 00:00:00 2001 From: Angela Yu Date: Fri, 7 Nov 2025 16:16:42 -0500 Subject: [PATCH 10/12] checkstyle --- src/main/java/com/patina/codebloom/api/duel/DuelController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/patina/codebloom/api/duel/DuelController.java b/src/main/java/com/patina/codebloom/api/duel/DuelController.java index 8ce2e77cf..b08df1cb6 100644 --- a/src/main/java/com/patina/codebloom/api/duel/DuelController.java +++ b/src/main/java/com/patina/codebloom/api/duel/DuelController.java @@ -74,7 +74,7 @@ public ResponseEntity> leaveParty() { @ApiResponse(responseCode = "400", description = "Player is already in a lobby") @ApiResponse(responseCode = "401", description = "User not authenticated") @PostMapping("/party/create") - public ResponseEntity> createParty( @Protected final AuthenticationObject authenticationObject) { + public ResponseEntity> createParty(@Protected final AuthenticationObject authenticationObject) { if (env.isProd()) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Endpoint is currently non-functional"); } From 7d28ca68fece77fab8e02a1ebc7a27af51987090 Mon Sep 17 00:00:00 2001 From: Angela Yu Date: Sun, 9 Nov 2025 12:03:02 -0500 Subject: [PATCH 11/12] duel tests for create party --- .../codebloom/component/DuelManagerTest.java | 297 ++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 src/test/java/com/patina/codebloom/component/DuelManagerTest.java diff --git a/src/test/java/com/patina/codebloom/component/DuelManagerTest.java b/src/test/java/com/patina/codebloom/component/DuelManagerTest.java new file mode 100644 index 000000000..cd429a515 --- /dev/null +++ b/src/test/java/com/patina/codebloom/component/DuelManagerTest.java @@ -0,0 +1,297 @@ +package com.patina.codebloom.component; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.OffsetDateTime; +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.http.ResponseEntity; + +import com.github.javafaker.Faker; +import com.patina.codebloom.api.duel.DuelController; +import com.patina.codebloom.common.db.models.lobby.Lobby; +import com.patina.codebloom.common.db.models.lobby.LobbyStatus; +import com.patina.codebloom.common.db.models.lobby.player.LobbyPlayer; +import com.patina.codebloom.common.db.models.user.User; +import com.patina.codebloom.common.db.repos.lobby.LobbyRepository; +import com.patina.codebloom.common.db.repos.lobby.player.LobbyPlayerRepository; +import com.patina.codebloom.common.dto.ApiResponder; +import com.patina.codebloom.common.dto.Empty; +import com.patina.codebloom.common.env.Env; +import com.patina.codebloom.common.security.AuthenticationObject; + +public class DuelManagerTest { + private final DuelController duelController; + private final Faker faker; + + private LobbyRepository lobbyRepository = mock(LobbyRepository.class); + private LobbyPlayerRepository lobbyPlayerRepository = mock(LobbyPlayerRepository.class); + private Env env = mock(Env.class); + + public DuelManagerTest() { + this.duelController = new DuelController(env, null, lobbyRepository, lobbyPlayerRepository); + this.faker = Faker.instance(); + } + + private String randomUUID() { + return UUID.randomUUID().toString(); + } + + private User createRandomUser() { + return User.builder() + .id(randomUUID()) + .discordId(String.valueOf(faker.number().randomNumber(18, true))) + .discordName(faker.name().username()) + .leetcodeUsername(faker.name().username()) + .admin(false) + .verifyKey(faker.crypto().md5()) + .build(); + } + + private AuthenticationObject createAuthenticationObject(final User user) { + return new AuthenticationObject(user, null); + } + + @Test + void testCreatePartySuccessWhenUserNotInParty() { + when(env.isProd()).thenReturn(false); + + User user = createRandomUser(); + AuthenticationObject authObj = createAuthenticationObject(user); + + when(lobbyPlayerRepository.findLobbyPlayerByPlayerId(user.getId())) + .thenReturn(null); + + ResponseEntity> response = duelController.createParty(authObj); + + assertEquals(200, response.getStatusCode().value()); + assertTrue(response.getBody().isSuccess()); + assertTrue(response.getBody().getPayload() instanceof Empty); + + ArgumentCaptor lobbyCaptor = ArgumentCaptor.forClass(Lobby.class); + ArgumentCaptor playerCaptor = ArgumentCaptor.forClass(LobbyPlayer.class); + + verify(lobbyRepository, times(1)).createLobby(lobbyCaptor.capture()); + verify(lobbyPlayerRepository, times(1)).createLobbyPlayer(playerCaptor.capture()); + + Lobby createdLobby = lobbyCaptor.getValue(); + assertNotNull(createdLobby.getJoinCode()); + assertEquals(6, createdLobby.getJoinCode().length()); + assertEquals(LobbyStatus.AVAILABLE, createdLobby.getStatus()); + assertEquals(1, createdLobby.getPlayerCount()); + assertNull(createdLobby.getWinnerId()); + assertNotNull(createdLobby.getExpiresAt()); + + LobbyPlayer createdPlayer = playerCaptor.getValue(); + assertEquals(user.getId(), createdPlayer.getPlayerId()); + assertEquals(0, createdPlayer.getPoints()); + } + + @Test + void testCreatePartyFailureWhenUserAlreadyInParty() { + when(env.isProd()).thenReturn(false); + + User user = createRandomUser(); + AuthenticationObject authObj = createAuthenticationObject(user); + + LobbyPlayer existingLobbyPlayer = LobbyPlayer.builder() + .id(randomUUID()) + .lobbyId(randomUUID()) + .playerId(user.getId()) + .points(100) + .build(); + + when(lobbyPlayerRepository.findLobbyPlayerByPlayerId(user.getId())) + .thenReturn(existingLobbyPlayer); + + ResponseEntity> response = duelController.createParty(authObj); + + assertEquals(400, response.getStatusCode().value()); + assertFalse(response.getBody().isSuccess()); + assertEquals("You are already in a lobby. Please leave your current lobby before creating a new one.", + response.getBody().getMessage()); + + verify(lobbyRepository, times(0)).createLobby(any()); + verify(lobbyPlayerRepository, times(0)).createLobbyPlayer(any()); + } + + @Test + void testMultipleUsersCanCreatePartiesIndependently() { + when(env.isProd()).thenReturn(false); + + User user1 = createRandomUser(); + User user2 = createRandomUser(); + AuthenticationObject authObj1 = createAuthenticationObject(user1); + AuthenticationObject authObj2 = createAuthenticationObject(user2); + + when(lobbyPlayerRepository.findLobbyPlayerByPlayerId(user1.getId())) + .thenReturn(null); + when(lobbyPlayerRepository.findLobbyPlayerByPlayerId(user2.getId())) + .thenReturn(null); + + ResponseEntity> response1 = duelController.createParty(authObj1); + ResponseEntity> response2 = duelController.createParty(authObj2); + + assertEquals(200, response1.getStatusCode().value()); + assertEquals(200, response2.getStatusCode().value()); + assertTrue(response1.getBody().isSuccess()); + assertTrue(response2.getBody().isSuccess()); + + ArgumentCaptor lobbyCaptor = ArgumentCaptor.forClass(Lobby.class); + ArgumentCaptor playerCaptor = ArgumentCaptor.forClass(LobbyPlayer.class); + + verify(lobbyRepository, times(2)).createLobby(lobbyCaptor.capture()); + verify(lobbyPlayerRepository, times(2)).createLobbyPlayer(playerCaptor.capture()); + + var lobbyPlayers = playerCaptor.getAllValues(); + assertEquals(user1.getId(), lobbyPlayers.get(0).getPlayerId()); + assertEquals(user2.getId(), lobbyPlayers.get(1).getPlayerId()); + } + + @Test + void testEachPartyHasUniqueJoinCode() { + when(env.isProd()).thenReturn(false); + + User user1 = createRandomUser(); + User user2 = createRandomUser(); + AuthenticationObject authObj1 = createAuthenticationObject(user1); + AuthenticationObject authObj2 = createAuthenticationObject(user2); + + when(lobbyPlayerRepository.findLobbyPlayerByPlayerId(user1.getId())) + .thenReturn(null); + when(lobbyPlayerRepository.findLobbyPlayerByPlayerId(user2.getId())) + .thenReturn(null); + + duelController.createParty(authObj1); + duelController.createParty(authObj2); + + ArgumentCaptor lobbyCaptor = ArgumentCaptor.forClass(Lobby.class); + verify(lobbyRepository, times(2)).createLobby(lobbyCaptor.capture()); + + var lobbies = lobbyCaptor.getAllValues(); + assertNotEquals(lobbies.get(0).getJoinCode(), lobbies.get(1).getJoinCode()); + } + + @Test + void testPartyExpirationTimeIsSet30MinutesInFuture() { + when(env.isProd()).thenReturn(false); + + User user = createRandomUser(); + AuthenticationObject authObj = createAuthenticationObject(user); + + when(lobbyPlayerRepository.findLobbyPlayerByPlayerId(user.getId())) + .thenReturn(null); + + OffsetDateTime beforeCreation = OffsetDateTime.now(); + + duelController.createParty(authObj); + + OffsetDateTime afterCreation = OffsetDateTime.now(); + + ArgumentCaptor lobbyCaptor = ArgumentCaptor.forClass(Lobby.class); + verify(lobbyRepository, times(1)).createLobby(lobbyCaptor.capture()); + + Lobby createdLobby = lobbyCaptor.getValue(); + OffsetDateTime expiresAt = createdLobby.getExpiresAt(); + + OffsetDateTime expectedExpiration = beforeCreation.plusMinutes(30); + assertTrue(expiresAt.isAfter(expectedExpiration.minusSeconds(5)), + "Expiration time should be after expected 30 minute mark"); + assertTrue(expiresAt.isBefore(afterCreation.plusMinutes(30).plusSeconds(5)), + "Expiration time should be before expected 30 minute mark plus buffer"); + } + + @Test + void testCreatePartyFailsInProductionEnvironment() { + when(env.isProd()).thenReturn(true); + + User user = createRandomUser(); + AuthenticationObject authObj = createAuthenticationObject(user); + + org.springframework.web.server.ResponseStatusException exception = assertThrows( + org.springframework.web.server.ResponseStatusException.class, + () -> duelController.createParty(authObj)); + + assertEquals(403, exception.getStatusCode().value()); + assertEquals("Endpoint is currently non-functional", exception.getReason()); + + verify(lobbyPlayerRepository, times(0)).findLobbyPlayerByPlayerId(any()); + verify(lobbyRepository, times(0)).createLobby(any()); + verify(lobbyPlayerRepository, times(0)).createLobbyPlayer(any()); + } + + @Test + void testNewLobbyPlayerStartsWithZeroPoints() { + when(env.isProd()).thenReturn(false); + + User user = createRandomUser(); + AuthenticationObject authObj = createAuthenticationObject(user); + + when(lobbyPlayerRepository.findLobbyPlayerByPlayerId(user.getId())) + .thenReturn(null); + + ResponseEntity> response = duelController.createParty(authObj); + + assertEquals(200, response.getStatusCode().value()); + + ArgumentCaptor playerCaptor = ArgumentCaptor.forClass(LobbyPlayer.class); + verify(lobbyPlayerRepository, times(1)).createLobbyPlayer(playerCaptor.capture()); + + LobbyPlayer createdPlayer = playerCaptor.getValue(); + assertEquals(0, createdPlayer.getPoints()); + } + + @Test + void testCreatePartyInitializesLobbyWithoutWinner() { + when(env.isProd()).thenReturn(false); + + User user = createRandomUser(); + AuthenticationObject authObj = createAuthenticationObject(user); + + when(lobbyPlayerRepository.findLobbyPlayerByPlayerId(user.getId())) + .thenReturn(null); + + ResponseEntity> response = duelController.createParty(authObj); + + assertEquals(200, response.getStatusCode().value()); + + ArgumentCaptor lobbyCaptor = ArgumentCaptor.forClass(Lobby.class); + verify(lobbyRepository, times(1)).createLobby(lobbyCaptor.capture()); + + Lobby createdLobby = lobbyCaptor.getValue(); + assertNull(createdLobby.getWinnerId()); + } + + @Test + void testSuccessResponseContainsJoinCode() { + when(env.isProd()).thenReturn(false); + + User user = createRandomUser(); + AuthenticationObject authObj = createAuthenticationObject(user); + + when(lobbyPlayerRepository.findLobbyPlayerByPlayerId(user.getId())) + .thenReturn(null); + + ResponseEntity> response = duelController.createParty(authObj); + + assertEquals(200, response.getStatusCode().value()); + assertTrue(response.getBody().isSuccess()); + assertTrue(response.getBody().getMessage().contains("Lobby created successfully"), + "Message should indicate successful lobby creation"); + assertTrue(response.getBody().getMessage().contains("join code"), + "Message should mention join code"); + } +} From 31da6bc64420b5c3b6ffa12fd8b5e58c3ec94730 Mon Sep 17 00:00:00 2001 From: Angela Yu Date: Sun, 9 Nov 2025 12:20:35 -0500 Subject: [PATCH 12/12] moved files --- src/main/java/com/patina/codebloom/api/duel/DuelController.java | 2 +- .../common/{util => utils/duel}/PartyCodeGenerator.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/main/java/com/patina/codebloom/common/{util => utils/duel}/PartyCodeGenerator.java (93%) diff --git a/src/main/java/com/patina/codebloom/api/duel/DuelController.java b/src/main/java/com/patina/codebloom/api/duel/DuelController.java index b08df1cb6..9b390f89e 100644 --- a/src/main/java/com/patina/codebloom/api/duel/DuelController.java +++ b/src/main/java/com/patina/codebloom/api/duel/DuelController.java @@ -22,7 +22,7 @@ import com.patina.codebloom.common.security.AuthenticationObject; import com.patina.codebloom.common.security.annotation.Protected; import com.patina.codebloom.common.time.StandardizedOffsetDateTime; -import com.patina.codebloom.common.util.PartyCodeGenerator; +import com.patina.codebloom.common.utils.duel.PartyCodeGenerator; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; diff --git a/src/main/java/com/patina/codebloom/common/util/PartyCodeGenerator.java b/src/main/java/com/patina/codebloom/common/utils/duel/PartyCodeGenerator.java similarity index 93% rename from src/main/java/com/patina/codebloom/common/util/PartyCodeGenerator.java rename to src/main/java/com/patina/codebloom/common/utils/duel/PartyCodeGenerator.java index 3bce6eb67..884dc163c 100644 --- a/src/main/java/com/patina/codebloom/common/util/PartyCodeGenerator.java +++ b/src/main/java/com/patina/codebloom/common/utils/duel/PartyCodeGenerator.java @@ -1,4 +1,4 @@ -package com.patina.codebloom.common.util; +package com.patina.codebloom.common.utils.duel; import java.security.SecureRandom;