diff --git a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/account/user/GuestUserRepository.java b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/account/user/GuestUserRepository.java index 526313f..6622c6d 100644 --- a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/account/user/GuestUserRepository.java +++ b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/account/user/GuestUserRepository.java @@ -8,7 +8,7 @@ @Repository public class GuestUserRepository implements UserRepository { - private final Map guestUsers = new HashMap<>(); + public final Map guestUsers = new HashMap<>(); @Override @NonNull diff --git a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/error/ControllerExceptionHandler.java b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/error/ControllerExceptionHandler.java index f0000bf..c2a0e1e 100644 --- a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/error/ControllerExceptionHandler.java +++ b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/error/ControllerExceptionHandler.java @@ -8,7 +8,6 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -import tools.jackson.databind.exc.InvalidTypeIdException; @Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice @@ -29,11 +28,6 @@ public class ControllerExceptionHandler extends ResponseEntityExceptionHandler { return responseOf(HttpServletResponse.SC_BAD_REQUEST, exception); } - @ExceptionHandler(InvalidTypeIdException.class) - public ResponseEntity<@NonNull String> handleInvalidType(InvalidTypeIdException exception) { - return ResponseEntity.badRequest().body("Unknown DTO type."); - } - private ResponseEntity<@NonNull ErrorResponse> responseOf(int status, Exception exception) { return ResponseEntity .status(status) diff --git a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/GameController.java b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/GameController.java index 9a20936..c52b4ef 100644 --- a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/GameController.java +++ b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/GameController.java @@ -29,7 +29,7 @@ public class GameController { private final GameManager gameManager; @GetMapping("/{gameUUID}/lobby") - private LobbyCodeResponse lobby(@PathVariable UUID gameUUID) throws MatchNotFoundException { + public LobbyCodeResponse lobby(@PathVariable UUID gameUUID) throws MatchNotFoundException { return new LobbyCodeResponse(lobbyManager.findLobbyByMatch(gameUUID)); } @@ -73,7 +73,7 @@ public Map grantedResources(@PathVariable UUID gameUUID, } @PostMapping("/{gameUUID}/makeMove") - private void playerAction(@PathVariable UUID gameUUID, + public void playerAction(@PathVariable UUID gameUUID, @RequestBody PlayerActionDTO request ) throws InvalidBuildRequestException, MoveHasntBeenImplementedException { gameManager.handlePlayerAction(gameUUID, AuthUtils.getAuthenticatedUser(), request); diff --git a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/board/StructureFactory.java b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/board/StructureFactory.java index c4c667f..cd2b891 100644 --- a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/board/StructureFactory.java +++ b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/board/StructureFactory.java @@ -34,7 +34,7 @@ public static void randomlyBuildInitialStructures(Match match, BuildingABuilding Set> corners = Set.copyOf(validator.getCorners()); for (PlayerRepresentation player: match.getPlayers().getPlayers()){ for (int i = 0; i < 2; i++){ // Place for each player two "SETTLEMENT", each with a "STREET" - if (attempts >= 100) throw new TooLittleSpaceException(); + if (attempts >= 500) throw new TooLittleSpaceException(); int rand = new SecureRandom().nextInt(corners.size()); List settlementPos = new ArrayList<>(corners).get(rand); BuildActionDTO settlement = new BuildActionDTO(StructureType.SETTLEMENT, settlementPos); diff --git a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/error/MissingAxialPositionsException.java b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/error/MissingAxialPositionsException.java index 73fe1c5..65a127a 100644 --- a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/error/MissingAxialPositionsException.java +++ b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/error/MissingAxialPositionsException.java @@ -4,7 +4,7 @@ import de.hexfieldsstudio.hexfieldsdominion.game.types.StructureType; public class MissingAxialPositionsException extends InvalidDtoException { - public MissingAxialPositionsException(StructureType type, int recievedAxialPositions) { - super("Structure type '%s' with '%s' Axial Positions is invalid, expected: '%s'".formatted(type.name(), type.getPosAmount(), recievedAxialPositions)); + public MissingAxialPositionsException(StructureType type, int receivedAxialPositions) { + super("Structure type '%s' with '%s' Axial Positions is invalid, expected: '%s'".formatted(type.name(), type.getPosAmount(), receivedAxialPositions)); } } diff --git a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/player/GamePlayers.java b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/player/GamePlayers.java index 30a3f34..dd0b76c 100644 --- a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/player/GamePlayers.java +++ b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/player/GamePlayers.java @@ -26,7 +26,8 @@ public GamePlayers(Lobby lobby) { } private List createPlayerRepresentationsForLobby(Lobby lobby) { - return lobby.getPlayers().stream() + // new ArrayList to prevent ConcurrentModificationException + return new ArrayList<>(lobby.getPlayers()).stream() .map(PlayerRepresentation::new) .toList(); } diff --git a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/player/PlayerHueFactory.java b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/player/PlayerHueFactory.java index 2b6985d..82a37d8 100644 --- a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/player/PlayerHueFactory.java +++ b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/player/PlayerHueFactory.java @@ -8,6 +8,6 @@ public class PlayerHueFactory { public int generateHueFromHash(String username) { String saltedUsername = username + "@hexfields"; // Add a salt to ensure more variability int hash = saltedUsername.hashCode(); - return hash % 360; // Extract hue component (0-360) + return Math.abs(hash % 360); // Extract hue component (0-360) } } \ No newline at end of file diff --git a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/trading/TradingHandler.java b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/trading/TradingHandler.java index d8d918e..32b8cbd 100644 --- a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/trading/TradingHandler.java +++ b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/game/trading/TradingHandler.java @@ -15,7 +15,7 @@ public class TradingHandler { - private static final int GIVE_GET_RATIO = 4; // (e.g. give:get 4:1 -> 4 / 1) + public static final int GIVE_GET_RATIO = 4; // (e.g. give:get 4:1 -> 4 / 1) @Getter private final Map playerTrades = new HashMap<>(); diff --git a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/lobby/Lobby.java b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/lobby/Lobby.java index 031a20b..cb77bde 100644 --- a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/lobby/Lobby.java +++ b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/lobby/Lobby.java @@ -15,7 +15,7 @@ public class Lobby implements NoHeartbeatListener { private final HeartbeatHandler heartbeatHandler; - private final List players = new ArrayList<>(); + private final List players = Collections.synchronizedList(new ArrayList<>()); private boolean hasAccountPlayer = false; private int nextPlayerId = 0; @Setter diff --git a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyController.java b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyController.java index 97fac71..a94da36 100644 --- a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyController.java +++ b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyController.java @@ -26,6 +26,8 @@ @RequiredArgsConstructor public class LobbyController { + public static final int BOARD_RADIUS = 3; // TODO: load boardRadius from configuration in the future + private final LobbyManager lobbyManager; @PatchMapping(produces = "application/json") @@ -60,9 +62,11 @@ public void doesLobbyWithCodeExist(@PathVariable String lobbyCode, HttpServletRe } @PostMapping("/{lobbyCode}/heartbeat") - public void heartbeat(@PathVariable String lobbyCode, @RequestBody HeartbeatDTO dto) throws LobbyNotFoundException { + public void heartbeat(@PathVariable String lobbyCode, @RequestBody HeartbeatDTO dto, HttpServletResponse response) throws LobbyNotFoundException { Lobby lobby = lobbyManager.findOccupiedLobbyOrThrow(lobbyCode); lobby.getHeartbeatHandler().resetTimer(dto.playerId()); + + response.setStatus(HttpServletResponse.SC_OK); } @GetMapping("/{lobbyCode}/events") @@ -75,7 +79,7 @@ public SseEmitter lobbyEvents(@PathVariable String lobbyCode) { @PostMapping("/{lobbyCode}/match") public LobbyManager.CreatedMatchResponse match(@PathVariable String lobbyCode) throws LobbyNotFoundException, NotOwnerOfLobbyException, TooLittleSpaceException { Lobby lobby = lobbyManager.findOccupiedLobbyOrThrow(lobbyCode); - Match match = lobbyManager.createMatchForLobby(lobby, AuthUtils.getAuthenticatedUser()); + Match match = lobbyManager.createMatchForLobby(lobby, AuthUtils.getAuthenticatedUser(), BOARD_RADIUS); return new LobbyManager.CreatedMatchResponse(match); } diff --git a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyManager.java b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyManager.java index 93940d3..0a80e9c 100644 --- a/src/main/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyManager.java +++ b/src/main/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyManager.java @@ -95,12 +95,11 @@ private void notifyLobbyUpdate(Lobby lobby) { sendEvent(allEmitters(lobbyCode), "lobbyUpdate", players, lobbyCode); } - public Match createMatchForLobby(Lobby lobby, User user) throws TooLittleSpaceException, InvalidRadiusException, NotOwnerOfLobbyException { + public Match createMatchForLobby(Lobby lobby, User user, int boardRadius) throws TooLittleSpaceException, InvalidRadiusException, NotOwnerOfLobbyException { if (!lobby.isOwner(user.getUsername())) { throw new NotOwnerOfLobbyException(); } - int boardRadius = 3; // TODO: load boardRadius from configuration in the future if (!(3 <= boardRadius && boardRadius <= 6)) throw new InvalidRadiusException(boardRadius); // random uuid could be replaced in the future to ensure uniqueness @@ -160,7 +159,7 @@ public JoinedLobbyResponse(Player player, Lobby lobby) { this(new CreatedPlayer(player), lobby.isOwner(player.getUsername())); } - private record CreatedPlayer(String username, int id, boolean isAccount) { + public record CreatedPlayer(String username, int id, boolean isAccount) { public CreatedPlayer(Player player) { this(player.getUsername(), player.getId(), player.isAccount()); } diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/RootControllerTest.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/RootControllerTest.java new file mode 100644 index 0000000..e721447 --- /dev/null +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/RootControllerTest.java @@ -0,0 +1,36 @@ +package de.hexfieldsstudio.hexfieldsdominion; + +import de.hexfieldsstudio.hexfieldsdominion.account.*; +import de.hexfieldsstudio.hexfieldsdominion.account.dto.LoginDTO; +import de.hexfieldsstudio.hexfieldsdominion.account.dto.RegisterDTO; +import de.hexfieldsstudio.hexfieldsdominion.account.token.SseTokenService; +import de.hexfieldsstudio.hexfieldsdominion.account.user.User; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.NonNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; + +import java.util.Optional; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class RootControllerTest { + + @InjectMocks + private RootController rootController; + + @Test + void testRoot() { + assertEquals("OK", rootController.root()); + } + +} diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/TestUtils.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/TestUtils.java new file mode 100644 index 0000000..4b3d6b3 --- /dev/null +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/TestUtils.java @@ -0,0 +1,13 @@ +package de.hexfieldsstudio.hexfieldsdominion; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestUtils { + + public static void assertLobbyCodeValid(String lobbyCode) { + assertNotNull(lobbyCode); + assertEquals(7, lobbyCode.length()); + assertTrue(lobbyCode.matches("^[A-Z0-9]+$")); + } + +} diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/account/AuthUtilsIT.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/account/AuthUtilsIT.java index 944c6c5..c44bb25 100644 --- a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/account/AuthUtilsIT.java +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/account/AuthUtilsIT.java @@ -7,8 +7,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; @SpringBootTest class AuthUtilsIT { @@ -31,14 +30,14 @@ void testGetAuthenticatedUserSuccess() { void testGetAuthenticatedUserFailNoAuthentication() { SecurityContextHolder.getContext().setAuthentication(null); - assertThrows(RuntimeException.class, AuthUtils::getAuthenticatedUser); + assertThrowsExactly(RuntimeException.class, AuthUtils::getAuthenticatedUser); } @Test void testGetAuthenticatedUserFailNotUser() { SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("invalid", null)); - assertThrows(RuntimeException.class, AuthUtils::getAuthenticatedUser); + assertThrowsExactly(RuntimeException.class, AuthUtils::getAuthenticatedUser); } } diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/account/AuthenticationServiceIT.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/account/AuthenticationServiceIT.java index d3e438d..9426f1a 100644 --- a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/account/AuthenticationServiceIT.java +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/account/AuthenticationServiceIT.java @@ -25,6 +25,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.crypto.password.PasswordEncoder; +import java.util.Arrays; import java.util.Optional; import java.util.stream.Stream; @@ -108,7 +109,7 @@ void testRegisterSuccess() { void testRegisterFailInvalidCredentials(String username, String password) { RegisterDTO registerDTO = new RegisterDTO(username, password); - assertThrows(InvalidCharactersException.class, () -> authenticationService.register(registerDTO)); + assertThrowsExactly(InvalidCharactersException.class, () -> authenticationService.register(registerDTO)); assertFalse(allUserRepository.findByUsername(username).isPresent()); } @@ -118,7 +119,7 @@ void testRegisterFailUserAlreadyExists() { RegisterDTO registerDTO = new RegisterDTO("testuser", "somePw"); authenticationService.register(registerDTO); - assertThrows(UserAlreadyExistsException.class, () -> authenticationService.register(registerDTO)); + assertThrowsExactly(UserAlreadyExistsException.class, () -> authenticationService.register(registerDTO)); } @Test @@ -151,7 +152,7 @@ void testLoginSuccess() { void testLoginFailUnknownUser() { LoginDTO loginDTO = new LoginDTO("testuser", "testpw"); - assertThrows(InvalidCredentialsException.class, () -> authenticationService.login(loginDTO)); + assertThrowsExactly(InvalidCredentialsException.class, () -> authenticationService.login(loginDTO)); } @Test @@ -169,7 +170,7 @@ void testLoginFailInvalidPassword() { LoginDTO loginDTO = new LoginDTO(username, "otherPw"); - assertThrows(InvalidCredentialsException.class, () -> authenticationService.login(loginDTO)); + assertThrowsExactly(InvalidCredentialsException.class, () -> authenticationService.login(loginDTO)); } @ParameterizedTest @@ -315,10 +316,8 @@ static class RolesProvider implements ArgumentsProvider { @Override @NullMarked public Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) { - return Stream.of( - Arguments.of(Role.GUEST), - Arguments.of(Role.PLAYER) - ); + return Arrays.stream(Role.values()) + .map(Arguments::of); } } diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/account/AuthenticationServiceTest.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/account/AuthenticationServiceTest.java index 46f6e50..0f1f36c 100644 --- a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/account/AuthenticationServiceTest.java +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/account/AuthenticationServiceTest.java @@ -80,7 +80,7 @@ void testRegisterSuccess() { void testRegisterFailInvalidCharacters(String username, String password) { RegisterDTO registerDTO = new RegisterDTO(username, password); - assertThrows(InvalidCharactersException.class, () -> authenticationService.register(registerDTO)); + assertThrowsExactly(InvalidCharactersException.class, () -> authenticationService.register(registerDTO)); } @Test @@ -99,7 +99,7 @@ void testRegisterFailUserAlreadyExists() { when(userRepository.findByUsernameIgnoreCase(username)).thenReturn(Optional.of(user)); - assertThrows(UserAlreadyExistsException.class, () -> authenticationService.register(registerDTO)); + assertThrowsExactly(UserAlreadyExistsException.class, () -> authenticationService.register(registerDTO)); } @Test @@ -132,7 +132,7 @@ void testLoginFailUnknownUser() { when(userRepository.findByUsername(anyString())).thenReturn(Optional.empty()); - assertThrows(InvalidCredentialsException.class, () -> authenticationService.login(loginDTO)); + assertThrowsExactly(InvalidCredentialsException.class, () -> authenticationService.login(loginDTO)); } @Test @@ -152,7 +152,7 @@ void testLoginFailInvalidPassword() { when(userRepository.findByUsername(username)).thenReturn(Optional.of(user)); when(passwordEncoder.matches(anyString(), anyString())).thenReturn(false); - assertThrows(InvalidCredentialsException.class, () -> authenticationService.login(loginDTO)); + assertThrowsExactly(InvalidCredentialsException.class, () -> authenticationService.login(loginDTO)); } @Test diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/error/ControllerExceptionHandlerTest.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/error/ControllerExceptionHandlerTest.java index c56131a..36bb15d 100644 --- a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/error/ControllerExceptionHandlerTest.java +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/error/ControllerExceptionHandlerTest.java @@ -2,8 +2,10 @@ import de.hexfieldsstudio.hexfieldsdominion.account.error.InvalidCharactersException; import de.hexfieldsstudio.hexfieldsdominion.account.error.UserAlreadyExistsException; -import de.hexfieldsstudio.hexfieldsdominion.game.error.MatchNotFoundException; -import de.hexfieldsstudio.hexfieldsdominion.game.error.NotPlayersTurnException; +import de.hexfieldsstudio.hexfieldsdominion.game.error.*; +import de.hexfieldsstudio.hexfieldsdominion.game.error.NotEnoughResourcesException; +import de.hexfieldsstudio.hexfieldsdominion.game.types.PlayerActionType; +import de.hexfieldsstudio.hexfieldsdominion.game.types.StructureType; import de.hexfieldsstudio.hexfieldsdominion.lobby.error.InvalidRadiusException; import de.hexfieldsstudio.hexfieldsdominion.lobby.error.LobbyNotFoundException; import de.hexfieldsstudio.hexfieldsdominion.lobby.error.NotOwnerOfLobbyException; @@ -89,7 +91,13 @@ public Stream provideArguments(ParameterDeclarations parame Arguments.of(new InvalidCharactersException()), Arguments.of(new InvalidCharactersException()), Arguments.of(new UserAlreadyExistsException()), - Arguments.of(new InvalidRadiusException(0)) + Arguments.of(new InvalidRadiusException(0)), + Arguments.of(new InvalidBuildRequestException()), + Arguments.of(new TooLittleSpaceException()), + Arguments.of(new NotEnoughResourcesException()), + Arguments.of(new MoveHasntBeenImplementedException(PlayerActionType.BUILD)), + Arguments.of(new MissingAxialPositionsException(StructureType.SETTLEMENT, 1)), + Arguments.of(new InvalidDtoException("")) ); } } diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/GameControllerTest.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/GameControllerTest.java new file mode 100644 index 0000000..d6d4879 --- /dev/null +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/GameControllerTest.java @@ -0,0 +1,217 @@ +package de.hexfieldsstudio.hexfieldsdominion.game; + +import de.hexfieldsstudio.hexfieldsdominion.account.AuthUtils; +import de.hexfieldsstudio.hexfieldsdominion.account.user.Role; +import de.hexfieldsstudio.hexfieldsdominion.account.user.User; +import de.hexfieldsstudio.hexfieldsdominion.game.board.Field; +import de.hexfieldsstudio.hexfieldsdominion.game.board.GameBoard; +import de.hexfieldsstudio.hexfieldsdominion.game.board.StructureFactory; +import de.hexfieldsstudio.hexfieldsdominion.game.dto.PlayerActionDTO; +import de.hexfieldsstudio.hexfieldsdominion.game.error.MatchNotFoundException; +import de.hexfieldsstudio.hexfieldsdominion.game.types.ResourceType; +import de.hexfieldsstudio.hexfieldsdominion.game.types.StructureType; +import de.hexfieldsstudio.hexfieldsdominion.lobby.Lobby; +import de.hexfieldsstudio.hexfieldsdominion.lobby.LobbyManager; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class GameControllerTest { + + @InjectMocks + private GameController gameController; + + @Mock + private LobbyManager lobbyManager; + + @Mock + private GameManager gameManager; + + @Test + void testLobbyMatchExists() { + UUID uuid = UUID.randomUUID(); + String lobbyCode = UUID.randomUUID().toString(); + Lobby lobby = mock(Lobby.class); + when(lobby.getLobbyCode()).thenReturn(lobbyCode); + when(lobbyManager.findLobbyByMatch(uuid)).thenReturn(lobby); + + GameController.LobbyCodeResponse response = gameController.lobby(uuid); + + assertEquals(lobbyCode, response.lobbyCode()); + } + + @Test + void testLobbyMatchDoesNotExist() { + UUID uuid = UUID.randomUUID(); + when(lobbyManager.findLobbyByMatch(uuid)).thenThrow(new MatchNotFoundException(uuid)); + + assertThrowsExactly(MatchNotFoundException.class, () -> gameController.lobby(uuid)); + } + + @Test + void testFieldsMatchExists() { + UUID uuid = UUID.randomUUID(); + Lobby lobby = mock(Lobby.class); + when(lobbyManager.findLobbyByMatch(uuid)).thenReturn(lobby); + Match match = mock(Match.class); + when(lobby.getMatch()).thenReturn(match); + GameBoard gameBoard = mock(GameBoard.class); + when(match.getGameBoard()).thenReturn(gameBoard); + List fields = List.of( + mock(Field.class), + mock(Field.class) + ); + when(gameBoard.getFields()).thenReturn(fields); + + List fieldsFound = gameController.fields(uuid); + + assertEquals(fields, fieldsFound); + } + + @Test + void testFieldsMatchDoesNotExist() { + UUID uuid = UUID.randomUUID(); + when(lobbyManager.findLobbyByMatch(uuid)).thenThrow(new MatchNotFoundException(uuid)); + + assertThrowsExactly(MatchNotFoundException.class, () -> gameController.fields(uuid)); + } + + @Test + void testRecipes() { + EnumMap recipeTown = new EnumMap<>(ResourceType.class); + EnumMap recipeSettlement = new EnumMap<>(ResourceType.class); + EnumMap recipeStreet = new EnumMap<>(ResourceType.class); + + try (MockedStatic structureFactory = mockStatic(StructureFactory.class)) { + structureFactory.when(() -> StructureFactory.getRecipeForStructureType(StructureType.TOWN)).thenReturn(recipeTown); + structureFactory.when(() -> StructureFactory.getRecipeForStructureType(StructureType.SETTLEMENT)).thenReturn(recipeSettlement); + structureFactory.when(() -> StructureFactory.getRecipeForStructureType(StructureType.STREET)).thenReturn(recipeStreet); + + EnumMap> recipesResult = gameController.recipes(); + assertEquals(recipeTown, recipesResult.get(StructureType.TOWN)); + assertEquals(recipeSettlement, recipesResult.get(StructureType.SETTLEMENT)); + assertEquals(recipeStreet, recipesResult.get(StructureType.STREET)); + } + } + + @Test + void testGameEvents() { + User user = User.builder() + .username("testuser") + .role(Role.GUEST) + .build(); + UUID uuid = UUID.randomUUID(); + + SseEmitter expectedEmitter = mock(SseEmitter.class); + + try (MockedStatic authUtils = mockStatic(AuthUtils.class)) { + authUtils.when(AuthUtils::getAuthenticatedUser).thenReturn(user); + when(gameManager.subscribe(uuid, user.getUsername())).thenReturn(expectedEmitter); + + SseEmitter createdEmitter = gameController.gameEvents(uuid); + + assertEquals(expectedEmitter, createdEmitter); + } + } + + @Test + void testRollDice() { + User user = User.builder() + .username("testuser") + .role(Role.GUEST) + .build(); + UUID uuid = UUID.randomUUID(); + + try (MockedStatic authUtils = mockStatic(AuthUtils.class)) { + authUtils.when(AuthUtils::getAuthenticatedUser).thenReturn(user); + gameController.rollDice(uuid); + + verify(gameManager, times(1)).rollDice(uuid, user); + } + } + + @Test + void testEndTurn() { + User user = User.builder() + .username("testuser") + .role(Role.GUEST) + .build(); + UUID uuid = UUID.randomUUID(); + + try (MockedStatic authUtils = mockStatic(AuthUtils.class)) { + authUtils.when(AuthUtils::getAuthenticatedUser).thenReturn(user); + gameController.endTurn(uuid); + + verify(gameManager, times(1)).nextPlayersTurn(uuid, user); + } + } + + @Test + void testGrantedResourcesPresent() { + User user = User.builder() + .username("testuser") + .role(Role.GUEST) + .build(); + UUID uuid = UUID.randomUUID(); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + + Map grantedResources = new HashMap<>(); + when(gameManager.getGrantedResources(uuid, user)).thenReturn(Optional.of(grantedResources)); + + try (MockedStatic authUtils = mockStatic(AuthUtils.class)) { + authUtils.when(AuthUtils::getAuthenticatedUser).thenReturn(user); + + Map result = gameController.grantedResources(uuid, servletResponse); + assertEquals(result, grantedResources); + } + } + + @Test + void testGrantedResourcesEmpty() { + User user = User.builder() + .username("testuser") + .role(Role.GUEST) + .build(); + UUID uuid = UUID.randomUUID(); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + + when(gameManager.getGrantedResources(uuid, user)).thenReturn(Optional.empty()); + + try (MockedStatic authUtils = mockStatic(AuthUtils.class)) { + authUtils.when(AuthUtils::getAuthenticatedUser).thenReturn(user); + + Map result = gameController.grantedResources(uuid, servletResponse); + assertTrue(result.isEmpty()); + verify(servletResponse, times(1)).setStatus(HttpServletResponse.SC_NO_CONTENT); + } + } + + @Test + void testPlayerAction() { + User user = User.builder() + .username("testuser") + .role(Role.GUEST) + .build(); + UUID uuid = UUID.randomUUID(); + PlayerActionDTO dto = mock(PlayerActionDTO.class); + + try (MockedStatic authUtils = mockStatic(AuthUtils.class)) { + authUtils.when(AuthUtils::getAuthenticatedUser).thenReturn(user); + gameController.playerAction(uuid, dto); + + verify(gameManager, times(1)).handlePlayerAction(uuid, user, dto); + } + } + +} diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/GameManagerTest.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/GameManagerTest.java new file mode 100644 index 0000000..2b4ada1 --- /dev/null +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/GameManagerTest.java @@ -0,0 +1,614 @@ +package de.hexfieldsstudio.hexfieldsdominion.game; + +import de.hexfieldsstudio.hexfieldsdominion.account.user.Role; +import de.hexfieldsstudio.hexfieldsdominion.account.user.User; +import de.hexfieldsstudio.hexfieldsdominion.config.AppConfig; +import de.hexfieldsstudio.hexfieldsdominion.error.ForbiddenException; +import de.hexfieldsstudio.hexfieldsdominion.game.board.GameBoard; +import de.hexfieldsstudio.hexfieldsdominion.game.board.StructureFactory; +import de.hexfieldsstudio.hexfieldsdominion.game.dto.BuildActionDTO; +import de.hexfieldsstudio.hexfieldsdominion.game.dto.PlayerActionDTO; +import de.hexfieldsstudio.hexfieldsdominion.game.dto.TradeBankDTO; +import de.hexfieldsstudio.hexfieldsdominion.game.dto.TradePlayerDTO; +import de.hexfieldsstudio.hexfieldsdominion.game.error.InvalidBuildRequestException; +import de.hexfieldsstudio.hexfieldsdominion.game.error.MatchNotFoundException; +import de.hexfieldsstudio.hexfieldsdominion.game.error.MoveHasntBeenImplementedException; +import de.hexfieldsstudio.hexfieldsdominion.game.error.NotPlayersTurnException; +import de.hexfieldsstudio.hexfieldsdominion.game.player.GamePlayers; +import de.hexfieldsstudio.hexfieldsdominion.game.player.Player; +import de.hexfieldsstudio.hexfieldsdominion.game.player.PlayerRepresentation; +import de.hexfieldsstudio.hexfieldsdominion.game.trading.TradingHandler; +import de.hexfieldsstudio.hexfieldsdominion.game.types.PlayerActionType; +import de.hexfieldsstudio.hexfieldsdominion.game.types.ResourceType; +import de.hexfieldsstudio.hexfieldsdominion.game.types.StructureType; +import de.hexfieldsstudio.hexfieldsdominion.lobby.Lobby; +import de.hexfieldsstudio.hexfieldsdominion.lobby.LobbyManager; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class GameManagerTest { + + private static final int POINTS_REQUIRED_TO_WIN = 10; + + @InjectMocks + private GameManager gameManager; + + @Mock + private LobbyManager lobbyManager; + + @Test + void testRollDice() { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + UUID uuid = UUID.randomUUID(); + + Lobby lobby = mock(Lobby.class); + Match match = mock(Match.class); + when(lobby.getMatch()).thenReturn(match); + GamePlayers players = mock(GamePlayers.class); + when(match.getPlayers()).thenReturn(players); + GameBoard gameBoard = mock(GameBoard.class); + when(match.getGameBoard()).thenReturn(gameBoard); + when(gameBoard.getStructures()).thenReturn(new ArrayList<>()); + + when(lobbyManager.findLobbyByMatch(uuid)).thenReturn(lobby); + when(players.isPlayersTurn(user)).thenReturn(true); + when(match.isRolledDiceThisTurn()).thenReturn(false); + + gameManager.rollDice(uuid, user); + + verify(match, times(1)).setRolledDiceThisTurn(anyBoolean()); + verify(match).setRolledDiceThisTurn(true); + verify(match, times(1)).setCurrentDiceResult(any()); + verify(match, times(1)).grantResourcesForDiceResult(anyInt()); + } + + @Test + void testRollDiceNotPlayersTurn() { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + UUID uuid = UUID.randomUUID(); + + this.testNotPlayersTurn(uuid, user, () -> gameManager.rollDice(uuid, user)); + } + + private void testNotPlayersTurn(UUID uuid, User user, Runnable action) { + Lobby lobby = mock(Lobby.class); + Match match = mock(Match.class); + when(lobby.getMatch()).thenReturn(match); + GamePlayers players = mock(GamePlayers.class); + when(match.getPlayers()).thenReturn(players); + + when(lobbyManager.findLobbyByMatch(uuid)).thenReturn(lobby); + when(players.isPlayersTurn(user)).thenReturn(false); + + assertThrowsExactly(NotPlayersTurnException.class, action::run); + } + + @Test + void testRollDiceAlreadyRolledThisTurn() { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + UUID uuid = UUID.randomUUID(); + + Lobby lobby = mock(Lobby.class); + Match match = mock(Match.class); + when(lobby.getMatch()).thenReturn(match); + GamePlayers players = mock(GamePlayers.class); + when(match.getPlayers()).thenReturn(players); + + when(lobbyManager.findLobbyByMatch(uuid)).thenReturn(lobby); + when(players.isPlayersTurn(user)).thenReturn(true); + when(match.isRolledDiceThisTurn()).thenReturn(true); + + Exception exception = assertThrowsExactly(ForbiddenException.class, () -> gameManager.rollDice(uuid, user)); + assertEquals("Already rolled dice.", exception.getMessage()); + } + + @Test + void testNextPlayersTurn() { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + UUID uuid = UUID.randomUUID(); + + Lobby lobby = mock(Lobby.class); + Match match = mock(Match.class); + when(lobby.getMatch()).thenReturn(match); + GamePlayers players = mock(GamePlayers.class); + when(match.getPlayers()).thenReturn(players); + GameBoard gameBoard = mock(GameBoard.class); + when(match.getGameBoard()).thenReturn(gameBoard); + when(gameBoard.getStructures()).thenReturn(new ArrayList<>()); + TradingHandler tradingHandler = mock(TradingHandler.class); + when(match.getTradingHandler()).thenReturn(tradingHandler); + + when(lobbyManager.findLobbyByMatch(uuid)).thenReturn(lobby); + when(players.isPlayersTurn(user)).thenReturn(true); + + gameManager.nextPlayersTurn(uuid, user); + + verify(match, times(1)).nextPlayersTurn(); + } + + @Test + void testNextPlayersTurnNotPlayersTurn() { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + UUID uuid = UUID.randomUUID(); + + this.testNotPlayersTurn(uuid, user, () -> gameManager.nextPlayersTurn(uuid, user)); + } + + @ParameterizedTest + @ValueSource(ints = {-3, -2, -1, 0, 1, 2, 3}) + void testAddPoints(int pointsToAdd) { + this.verifyNoWinnerAfterAddPoints(0, pointsToAdd); + } + + @ParameterizedTest + @ValueSource(ints = {-3, -2, -1, 0, 1, 2, 3}) + void testAddPointsWhenPlayerAlreadyHasPoints(int pointsToAdd) { + int initialPoints = 1; + this.verifyNoWinnerAfterAddPoints(initialPoints, pointsToAdd); + } + + @ParameterizedTest + @ValueSource(ints = {1, 2}) + void testAddPointsXUntilWin(int pointsUntilWin) { + this.verifyNoWinnerAfterAddPoints(0, POINTS_REQUIRED_TO_WIN - pointsUntilWin); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + void testAddPointsXMoreThanWin(int pointsMoreThanWin) { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + Player player = new Player(user, 0); + PlayerRepresentation playerRepresentation = new PlayerRepresentation(player); + + Match match = mock(Match.class); + GamePlayers players = mock(GamePlayers.class); + when(match.getPlayers()).thenReturn(players); + when(players.getPlayerForUser(user)).thenReturn(Optional.of(playerRepresentation)); + + gameManager.addPoints(match, user, POINTS_REQUIRED_TO_WIN + pointsMoreThanWin); + + verify(players, times(1)).setWinner(any()); + verify(players).setWinner(playerRepresentation); + assertEquals(POINTS_REQUIRED_TO_WIN + pointsMoreThanWin, playerRepresentation.getPoints()); + } + + @Test + void testAddPointsNoPlayerForUser() { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + + Match match = mock(Match.class); + GamePlayers players = mock(GamePlayers.class); + when(match.getPlayers()).thenReturn(players); + when(players.getPlayerForUser(user)).thenReturn(Optional.empty()); + + gameManager.addPoints(match, user, POINTS_REQUIRED_TO_WIN); + + verify(players, times(0)).setWinner(any()); + } + + private void verifyNoWinnerAfterAddPoints(int initialPoints, int pointsToAdd) { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + Player player = new Player(user, 0); + PlayerRepresentation playerRepresentation = new PlayerRepresentation(player); + playerRepresentation.setPoints(initialPoints); + + Match match = mock(Match.class); + GamePlayers players = mock(GamePlayers.class); + when(match.getPlayers()).thenReturn(players); + when(players.getPlayerForUser(user)).thenReturn(Optional.of(playerRepresentation)); + + gameManager.addPoints(match, user, pointsToAdd); + + verify(players, times(0)).setWinner(any()); + assertEquals(initialPoints + pointsToAdd, playerRepresentation.getPoints()); + } + + @Test + void testHandlePlayerActionBuild() { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + UUID uuid = UUID.randomUUID(); + BuildActionDTO dto = new BuildActionDTO( + StructureType.STREET, + List.of(AxialPosition.of(0, 0), AxialPosition.of(0, 1)) + ); + // is normally done automatically when a subtype is created for a received PlayerAction + dto.setType(PlayerActionType.BUILD); + + Lobby lobby = mock(Lobby.class); + Match match = mock(Match.class); + when(lobby.getMatch()).thenReturn(match); + GamePlayers players = mock(GamePlayers.class); + when(match.getPlayers()).thenReturn(players); + GameBoard gameBoard = mock(GameBoard.class); + when(match.getGameBoard()).thenReturn(gameBoard); + when(gameBoard.getStructures()).thenReturn(new ArrayList<>()); + + when(lobbyManager.findLobbyByMatch(uuid)).thenReturn(lobby); + when(players.isPlayersTurn(user)).thenReturn(true); + + BuildingABuildingValidator validator = mock(BuildingABuildingValidator.class); + when(match.getValidator()).thenReturn(validator); + + when(validator.validate(user, match, dto)).thenReturn(true); + + gameManager.handlePlayerAction(uuid, user, dto); + + this.verifyBuildBuildingStreet(match, user, dto); + } + + @Test + void testHandlePlayerActionBuildNotPlayersTurn() { + this.testHandlePlayerActionNotPlayersTurn(PlayerActionType.BUILD); + } + + @Test + void testHandlePlayerActionTradeBank() { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + UUID uuid = UUID.randomUUID(); + + TradeBankDTO dto = mock(TradeBankDTO.class); + when(dto.getType()).thenReturn(PlayerActionType.TRADE_BANK); + + Lobby lobby = mock(Lobby.class); + Match match = mock(Match.class); + when(lobby.getMatch()).thenReturn(match); + GamePlayers players = mock(GamePlayers.class); + when(match.getPlayers()).thenReturn(players); + GameBoard gameBoard = mock(GameBoard.class); + when(match.getGameBoard()).thenReturn(gameBoard); + when(gameBoard.getStructures()).thenReturn(new ArrayList<>()); + TradingHandler tradingHandler = mock(TradingHandler.class); + when(match.getTradingHandler()).thenReturn(tradingHandler); + + when(lobbyManager.findLobbyByMatch(uuid)).thenReturn(lobby); + when(players.isPlayersTurn(user)).thenReturn(true); + + gameManager.handlePlayerAction(uuid, user, dto); + + verify(tradingHandler, times(1)).tradeBank(any(), any(), any()); + verify(tradingHandler).tradeBank(user, match, dto); + } + + @Test + void testHandlePlayerActionTradeBankNotPlayersTurn() { + this.testHandlePlayerActionNotPlayersTurn(PlayerActionType.TRADE_BANK); + } + + @Test + void testHandlePlayerActionTradePlayer() { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + UUID uuid = UUID.randomUUID(); + + TradePlayerDTO dto = mock(TradePlayerDTO.class); + when(dto.getType()).thenReturn(PlayerActionType.TRADE_PLAYER); + + Lobby lobby = mock(Lobby.class); + Match match = mock(Match.class); + when(lobby.getMatch()).thenReturn(match); + GamePlayers players = mock(GamePlayers.class); + when(match.getPlayers()).thenReturn(players); + GameBoard gameBoard = mock(GameBoard.class); + when(match.getGameBoard()).thenReturn(gameBoard); + when(gameBoard.getStructures()).thenReturn(new ArrayList<>()); + TradingHandler tradingHandler = mock(TradingHandler.class); + when(match.getTradingHandler()).thenReturn(tradingHandler); + + when(lobbyManager.findLobbyByMatch(uuid)).thenReturn(lobby); + + gameManager.handlePlayerAction(uuid, user, dto); + + verify(tradingHandler, times(1)).handlePlayerTrade(any(), any(), any()); + verify(tradingHandler).handlePlayerTrade(user, match, dto); + } + + private void testHandlePlayerActionNotPlayersTurn(PlayerActionType playerActionType) { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + UUID uuid = UUID.randomUUID(); + + PlayerActionDTO dto = mock(PlayerActionDTO.class); + when(dto.getType()).thenReturn(playerActionType); + + this.testNotPlayersTurn(uuid, user, () -> gameManager.handlePlayerAction(uuid, user, dto)); + } + + //TODO: fix. Geht aus irgendnem Grund nicht wenn man alle Test in der Klasse auf einmal ausführt + //@Test + void testHandlePlayerActionNotImplemented() { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + UUID uuid = UUID.randomUUID(); + + try (MockedStatic mockedStatic = mockStatic(PlayerActionType.class)) { + PlayerActionType[] mockedActionTypes = new PlayerActionType[]{ + PlayerActionType.BUILD, + PlayerActionType.PICK_DICE_PAIR, + PlayerActionType.TRADE_BANK, + PlayerActionType.TRADE_PLAYER, + // additional entry so there is one index left to use + PlayerActionType.TRADE_PLAYER, + }; + mockedStatic.when(PlayerActionType::values).thenReturn(mockedActionTypes); + + PlayerActionType playerActionTypeUnknown = mock(PlayerActionType.class); + // use last index left + when(playerActionTypeUnknown.ordinal()).thenReturn(mockedActionTypes.length - 1); + + PlayerActionDTO dto = mock(PlayerActionDTO.class); + when(dto.getType()).thenReturn(playerActionTypeUnknown); + + assertThrowsExactly(MoveHasntBeenImplementedException.class, () -> gameManager.handlePlayerAction(uuid, user, dto)); + } + } + + @Test + void testBuildBuildingStreet() { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + BuildActionDTO dto = new BuildActionDTO( + StructureType.STREET, + List.of(AxialPosition.of(0, 0), AxialPosition.of(0, 1)) + ); + + Match match = mock(Match.class); + + BuildingABuildingValidator validator = mock(BuildingABuildingValidator.class); + when(match.getValidator()).thenReturn(validator); + + when(validator.validate(user, match, dto)).thenReturn(true); + + gameManager.buildBuilding(user, match, dto); + + this.verifyBuildBuildingStreet(match, user, dto); + } + + void verifyBuildBuildingStreet(Match match, User user, BuildActionDTO dto) { + verify(match, times(1)).buildBuilding(any(User.class), any(BuildActionDTO.class)); + verify(match).buildBuilding(user, dto); + verify(match, times(1)).letPlayerPayRecipe(any(User.class), any()); + verify(match).letPlayerPayRecipe(user, StructureFactory.getRecipeForStructureType(dto.getStructureType())); + } + + @Test + void testBuildBuildingSettlement() { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + BuildActionDTO dto = new BuildActionDTO( + StructureType.SETTLEMENT, + List.of(AxialPosition.of(0, 0), AxialPosition.of(0, 1), AxialPosition.of(1, 1)) + ); + + PlayerRepresentation player = mock(PlayerRepresentation.class); + Match match = mock(Match.class); + GamePlayers players = mock(GamePlayers.class); + when(match.getPlayers()).thenReturn(players); + when(players.getPlayerForUser(user)).thenReturn(Optional.of(player)); + BuildingABuildingValidator validator = mock(BuildingABuildingValidator.class); + when(match.getValidator()).thenReturn(validator); + + when(validator.validate(user, match, dto)).thenReturn(true); + + gameManager.buildBuilding(user, match, dto); + + verify(match, times(1)).buildBuilding(any(User.class), any(BuildActionDTO.class)); + verify(match).buildBuilding(user, dto); + verify(match, times(1)).letPlayerPayRecipe(any(User.class), any()); + verify(match).letPlayerPayRecipe(user, StructureFactory.getRecipeForStructureType(dto.getStructureType())); + verify(player, times(1)).addPoints(anyInt()); + verify(player).addPoints(1); + } + + @Test + void testBuildBuildingTown() { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + BuildActionDTO dto = new BuildActionDTO( + StructureType.TOWN, + List.of(AxialPosition.of(0, 0), AxialPosition.of(0, 1), AxialPosition.of(1, 1)) + ); + + PlayerRepresentation player = mock(PlayerRepresentation.class); + Match match = mock(Match.class); + GamePlayers players = mock(GamePlayers.class); + when(match.getPlayers()).thenReturn(players); + when(players.getPlayerForUser(user)).thenReturn(Optional.of(player)); + BuildingABuildingValidator validator = mock(BuildingABuildingValidator.class); + when(match.getValidator()).thenReturn(validator); + + when(validator.validate(user, match, dto)).thenReturn(true); + + gameManager.buildBuilding(user, match, dto); + + verify(match, times(1)).upgradeSettlementToTown(any(User.class), any(BuildActionDTO.class)); + verify(match).upgradeSettlementToTown(user, dto); + verify(match, times(1)).letPlayerPayRecipe(any(User.class), any()); + verify(match).letPlayerPayRecipe(user, StructureFactory.getRecipeForStructureType(dto.getStructureType())); + verify(player, times(1)).addPoints(anyInt()); + verify(player).addPoints(2); + } + + @Test + void testBuildBuildingInvalidBuildRequest() { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + BuildActionDTO dto = new BuildActionDTO( + StructureType.STREET, + List.of(AxialPosition.of(0, 0), AxialPosition.of(0, 1)) + ); + + Match match = mock(Match.class); + BuildingABuildingValidator validator = mock(BuildingABuildingValidator.class); + when(match.getValidator()).thenReturn(validator); + + when(validator.validate(user, match, dto)).thenReturn(false); + + assertThrowsExactly(InvalidBuildRequestException.class, () -> gameManager.buildBuilding(user, match, dto)); + } + + @Test + void testGetGrantedResources() { + UUID uuid = UUID.randomUUID(); + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + Player player = new Player(user, 0); + PlayerRepresentation playerRepresentation = new PlayerRepresentation(player); + Map grantedResourcesPlayer = mock(Map.class); + + Lobby lobby = mock(Lobby.class); + Match match = mock(Match.class); + Map> grantedResources = mock(Map.class); + when(lobby.getMatch()).thenReturn(match); + GamePlayers players = mock(GamePlayers.class); + when(match.getPlayers()).thenReturn(players); + GameBoard gameBoard = mock(GameBoard.class); + when(match.getGameBoard()).thenReturn(gameBoard); + when(gameBoard.getStructures()).thenReturn(new ArrayList<>()); + TradingHandler tradingHandler = mock(TradingHandler.class); + when(match.getTradingHandler()).thenReturn(tradingHandler); + + when(players.getPlayerForUser(user)).thenReturn(Optional.of(playerRepresentation)); + when(match.getGrantedResourcesThisTurn()).thenReturn(grantedResources); + when(grantedResources.get(playerRepresentation.getPublicId())).thenReturn(grantedResourcesPlayer); + when(lobbyManager.findLobbyByMatch(uuid)).thenReturn(lobby); + gameManager.subscribe(uuid, user.getUsername()); + + Optional> result = gameManager.getGrantedResources(uuid, user); + + assertTrue(result.isPresent()); + assertEquals(grantedResourcesPlayer, result.get()); + } + + @Test + void testGetGrantedResourcesPlayerNotFound() { + UUID uuid = UUID.randomUUID(); + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + + Lobby lobby = mock(Lobby.class); + Match match = mock(Match.class); + when(lobby.getMatch()).thenReturn(match); + GamePlayers players = mock(GamePlayers.class); + when(match.getPlayers()).thenReturn(players); + GameBoard gameBoard = mock(GameBoard.class); + when(match.getGameBoard()).thenReturn(gameBoard); + when(gameBoard.getStructures()).thenReturn(new ArrayList<>()); + TradingHandler tradingHandler = mock(TradingHandler.class); + when(match.getTradingHandler()).thenReturn(tradingHandler); + + when(lobbyManager.findLobbyByMatch(uuid)).thenReturn(lobby); + gameManager.subscribe(uuid, user.getUsername()); + + Optional> result = gameManager.getGrantedResources(uuid, user); + + assertFalse(result.isPresent()); + } + + @Test + void testSubscribeLobbyFound() { + UUID uuid = UUID.randomUUID(); + + Lobby lobby = mock(Lobby.class); + Match match = mock(Match.class); + when(lobby.getMatch()).thenReturn(match); + GamePlayers players = mock(GamePlayers.class); + when(match.getPlayers()).thenReturn(players); + GameBoard gameBoard = mock(GameBoard.class); + when(match.getGameBoard()).thenReturn(gameBoard); + when(gameBoard.getStructures()).thenReturn(new ArrayList<>()); + TradingHandler tradingHandler = mock(TradingHandler.class); + when(match.getTradingHandler()).thenReturn(tradingHandler); + + when(lobbyManager.findLobbyByMatch(uuid)).thenReturn(lobby); + + SseEmitter emitter = gameManager.subscribe(uuid, "someone"); + + assertNotNull(emitter); + } + + @Test + void testSubscribeLobbyNotFound() { + UUID uuid = UUID.randomUUID(); + when(lobbyManager.findLobbyByMatch(uuid)).thenThrow(MatchNotFoundException.class); + + assertThrowsExactly(MatchNotFoundException.class, () -> gameManager.subscribe(uuid, "someone")); + } + + @Test + void testOnUnsubscribe() { + UUID uuid = UUID.randomUUID(); + AppConfig appConfig = mock(AppConfig.class); + Lobby lobby = new Lobby(appConfig); + Player player = mock(Player.class); + lobby.getPlayers().add(player); + + when(player.getUsername()).thenReturn("someone"); + when(lobbyManager.findLobbyByMatch(uuid)).thenReturn(lobby); + + gameManager.onUnsubscribe(uuid, "someone"); + + assertTrue(lobby.getPlayers().isEmpty()); + } + +} diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/MatchTest.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/MatchTest.java new file mode 100644 index 0000000..92c63ef --- /dev/null +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/MatchTest.java @@ -0,0 +1,257 @@ +package de.hexfieldsstudio.hexfieldsdominion.game; + +import de.hexfieldsstudio.hexfieldsdominion.account.user.Role; +import de.hexfieldsstudio.hexfieldsdominion.account.user.User; +import de.hexfieldsstudio.hexfieldsdominion.game.board.Field; +import de.hexfieldsstudio.hexfieldsdominion.game.board.Structure; +import de.hexfieldsstudio.hexfieldsdominion.game.dto.BuildActionDTO; +import de.hexfieldsstudio.hexfieldsdominion.game.player.Player; +import de.hexfieldsstudio.hexfieldsdominion.game.player.PlayerRepresentation; +import de.hexfieldsstudio.hexfieldsdominion.game.types.ResourceType; +import de.hexfieldsstudio.hexfieldsdominion.game.types.StructureType; +import de.hexfieldsstudio.hexfieldsdominion.lobby.Lobby; +import de.hexfieldsstudio.hexfieldsdominion.lobby.LobbyController; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.*; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +public class MatchTest { + + private Match match; + + private Lobby lobby; + + @BeforeEach + void setupEach() { + lobby = mock(Lobby.class); + + match = new Match( + UUID.randomUUID(), + LobbyController.BOARD_RADIUS, + lobby + ); + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4, 5}) + void testNextPlayersTurn(int playersCount) { + List playersList = new ArrayList<>(); + for (int i = 0; i < playersCount; i++) { + User user = User.builder() + .username("someone" + i) + .role(Role.GUEST) + .id(i) + .build(); + Player player = new Player(user, i); + playersList.add(player); + } + when(lobby.getPlayers()).thenReturn(playersList); + + match = new Match( + UUID.randomUUID(), + LobbyController.BOARD_RADIUS, + lobby + ); + + int[] recordedTurnOrder = new int[playersCount]; + // record + for (int i = 0; i < playersCount; i++) { + recordedTurnOrder[i] = match.getPlayers().getPlayerCurrentTurn(); + match.nextPlayersTurn(); + } + // check against recorded order + for (int i = 0; i < playersCount; i++) { + assertEquals(recordedTurnOrder[i], match.getPlayers().getPlayerCurrentTurn()); + match.nextPlayersTurn(); + } + } + + @ParameterizedTest + @ValueSource(ints = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}) + void testGrantResourcesForDiceResult(int diceResult) { + this.createNewMatchWithPlayer(player -> { + List fields = match.getGameBoard().getFields(); + + Map expectedResourcesGranted = new HashMap<>(player.getResources()); + for (Field field : fields) { + if (field.numberChip() != diceResult) { + continue; + } + + for (Structure structure : match.getGameBoard().getStructures()) { + if (structure.getType() != StructureType.SETTLEMENT) { + continue; + } + + if (!structure.getPos().contains(field.pos())) { + continue; + } + + if (structure.getOwnerId() != player.getPublicId()) { + continue; + } + + expectedResourcesGranted.compute( + field.resource(), + (k, v) -> (v == null) ? 1 : v + 1 + ); + } + } + + match.grantResourcesForDiceResult(diceResult); + + assertEquals(expectedResourcesGranted.get(ResourceType.SHEEP), player.getResources().get(ResourceType.SHEEP)); + assertEquals(expectedResourcesGranted.get(ResourceType.WOOD), player.getResources().get(ResourceType.WOOD)); + assertEquals(expectedResourcesGranted.get(ResourceType.WHEAT), player.getResources().get(ResourceType.WHEAT)); + assertEquals(expectedResourcesGranted.get(ResourceType.BRICK), player.getResources().get(ResourceType.BRICK)); + }); + } + + @Test + void testBuildBuildingUserSuccess() { + this.createNewMatchWithPlayer(player -> { + // remove initial structures to prevent conflicts + match.getGameBoard().getStructures().clear(); + + StructureType structureType = StructureType.STREET; + List pos = List.of(new AxialPosition(0, 0), new AxialPosition(0, 1)); + BuildActionDTO dto = new BuildActionDTO(structureType, pos); + + match.buildBuilding(player.getPlayer().getUser(), dto); + + assertEquals(1, match.getGameBoard().getStructures().size()); + assertEquals(structureType, match.getGameBoard().getStructures().getLast().getType()); + assertEquals(pos, match.getGameBoard().getStructures().getLast().getPos()); + }); + } + + @Test + void testBuildBuildingUserFailNoPlayerForUser() { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + + // remove initial structures to prevent conflicts + match.getGameBoard().getStructures().clear(); + + StructureType structureType = StructureType.STREET; + List pos = List.of(new AxialPosition(0, 0), new AxialPosition(0, 1)); + BuildActionDTO dto = new BuildActionDTO(structureType, pos); + + match.buildBuilding(user, dto); + + assertTrue(match.getGameBoard().getStructures().isEmpty()); + } + + @Test + void testBuildBuildingPlayer() { + // remove initial structures to prevent conflicts + match.getGameBoard().getStructures().clear(); + + StructureType structureType = StructureType.STREET; + List pos = List.of(new AxialPosition(0, 0), new AxialPosition(0, 1)); + BuildActionDTO dto = new BuildActionDTO(structureType, pos); + PlayerRepresentation player = mock(PlayerRepresentation.class); + + match.buildBuilding(player, dto); + + assertEquals(1, match.getGameBoard().getStructures().size()); + assertEquals(structureType, match.getGameBoard().getStructures().getFirst().getType()); + assertEquals(pos, match.getGameBoard().getStructures().getFirst().getPos()); + } + + @Test + void testUpgradeSettlementToTownUserSuccess() { + this.createNewMatchWithPlayer(player -> { + // remove initial structures to prevent conflicts + match.getGameBoard().getStructures().clear(); + + List pos = List.of(new AxialPosition(0, 0), new AxialPosition(0, 1), new AxialPosition(1, 1)); + BuildActionDTO buildDto = new BuildActionDTO(StructureType.SETTLEMENT, pos); + BuildActionDTO upgradeDto = new BuildActionDTO(StructureType.TOWN, pos); + match.buildBuilding(player.getPlayer().getUser(), buildDto); + + match.upgradeSettlementToTown(player.getPlayer().getUser(), upgradeDto); + + assertEquals(1, match.getGameBoard().getStructures().size()); + assertEquals(StructureType.TOWN, match.getGameBoard().getStructures().getFirst().getType()); + assertEquals(pos, match.getGameBoard().getStructures().getFirst().getPos()); + }); + } + + @Test + void testUpgradeSettlementToTownPlayer() { + // remove initial structures to prevent conflicts + match.getGameBoard().getStructures().clear(); + + List pos = List.of(new AxialPosition(0, 0), new AxialPosition(0, 1), new AxialPosition(1, 1)); + BuildActionDTO buildDto = new BuildActionDTO(StructureType.SETTLEMENT, pos); + BuildActionDTO upgradeDto = new BuildActionDTO(StructureType.TOWN, pos); + PlayerRepresentation player = mock(PlayerRepresentation.class); + match.buildBuilding(player, buildDto); + + match.upgradeSettlementToTown(player, upgradeDto); + + assertEquals(1, match.getGameBoard().getStructures().size()); + assertEquals(StructureType.TOWN, match.getGameBoard().getStructures().getFirst().getType()); + assertEquals(pos, match.getGameBoard().getStructures().getFirst().getPos()); + } + + @Test + void testLetPlayerPayRecipe() { + this.createNewMatchWithPlayer(player -> { + // remove initial structures to prevent conflicts + match.getGameBoard().getStructures().clear(); + + match.letPlayerPayRecipe(player.getPlayer().getUser(), new HashMap<>(Map.of( + ResourceType.SHEEP, 0, + ResourceType.WOOD, 1, + ResourceType.WHEAT, 0, + ResourceType.BRICK, 1 + ))); + + assertEquals(0, player.getResources().get(ResourceType.SHEEP)); + assertEquals(-1, player.getResources().get(ResourceType.WOOD)); + assertEquals(0, player.getResources().get(ResourceType.WHEAT)); + assertEquals(-1, player.getResources().get(ResourceType.BRICK)); + }); + } + + private void createNewMatchWithPlayer(Consumer consumer) { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + when(lobby.getPlayers()).thenReturn(new ArrayList<>(List.of( + new Player(user, 0) + ))); + + match = new Match( + UUID.randomUUID(), + LobbyController.BOARD_RADIUS, + lobby + ); + + Optional playerOptional = match.getPlayers().getPlayerForUser(user); + assert playerOptional.isPresent(); + PlayerRepresentation player = playerOptional.get(); + player.getResources().putAll(Map.of( + ResourceType.SHEEP, 0, + ResourceType.WOOD, 0, + ResourceType.WHEAT, 0, + ResourceType.BRICK, 0 + )); + + consumer.accept(player); + } + +} diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/board/GameBoardTest.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/board/GameBoardTest.java new file mode 100644 index 0000000..957cd4d --- /dev/null +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/board/GameBoardTest.java @@ -0,0 +1,227 @@ +package de.hexfieldsstudio.hexfieldsdominion.game.board; + +import de.hexfieldsstudio.hexfieldsdominion.account.user.Role; +import de.hexfieldsstudio.hexfieldsdominion.account.user.User; +import de.hexfieldsstudio.hexfieldsdominion.game.AxialPosition; +import de.hexfieldsstudio.hexfieldsdominion.game.dto.BuildActionDTO; +import de.hexfieldsstudio.hexfieldsdominion.game.player.Player; +import de.hexfieldsstudio.hexfieldsdominion.game.player.PlayerRepresentation; +import de.hexfieldsstudio.hexfieldsdominion.game.types.ResourceType; +import de.hexfieldsstudio.hexfieldsdominion.game.types.StructureType; +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.support.ParameterDeclarations; +import org.mockito.MockedStatic; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mockStatic; + +public class GameBoardTest { + + private static final int BOARD_RADIUS = 3; + private static final Map RATIOS = Map.of( + ResourceType.WOOD, 0.3f, + ResourceType.BRICK, 0.2f, + ResourceType.WHEAT, 0.3f, + ResourceType.SHEEP, 0.2f + ); + + private GameBoard gameBoard; + + @BeforeAll + @SuppressWarnings("resource") // AutoClosable mockStatic + static void setup() { + List fields = List.of( + new Field(AxialPosition.of(1, 1), 2, ResourceType.BRICK), + new Field(AxialPosition.of(-1, -1), 11, ResourceType.WHEAT), + new Field(AxialPosition.of(2, 2), 5, ResourceType.SHEEP), + new Field(AxialPosition.of(-2, -2), 11, ResourceType.BRICK) + ); + + MockedStatic mockedFieldFactory = mockStatic(FieldFactory.class); + mockedFieldFactory.when(() -> FieldFactory.generateFields(BOARD_RADIUS, RATIOS)).thenReturn(fields); + } + + @BeforeEach + void setupEach() { + gameBoard = new GameBoard(BOARD_RADIUS); + } + + @ParameterizedTest + @ArgumentsSource(StructureTypesProvider.class) + void testAddStructure(StructureType structureType) { + PlayerRepresentation player = this.createPlayerRepresentation(); + + BuildActionDTO dto = new BuildActionDTO( + structureType, + List.of(AxialPosition.of(1, 1), AxialPosition.of(-1, -1)) + ); + + gameBoard.addStructure(player, dto); + + assertEquals(1, gameBoard.getStructures().size()); + Structure addedStructure = gameBoard.getStructures().getFirst(); + assertEquals(player.getPublicId(), addedStructure.getOwnerId()); + assertEquals(dto.getStructureType(), addedStructure.getType()); + assertEquals(dto.getPos(), addedStructure.getPos()); + } + + @Test + void testUpgradeSettlementToTown() { + PlayerRepresentation player = this.createPlayerRepresentation(); + + Structure structure = this.createBasicStructure(StructureType.SETTLEMENT); + gameBoard.getStructures().add(structure); + + BuildActionDTO dto = new BuildActionDTO( + StructureType.TOWN, + List.of(AxialPosition.of(1, 1)) + ); + + gameBoard.upgradeSettlementToTown(player, dto); + + assertEquals(1, gameBoard.getStructures().size()); + Structure storedStructure = gameBoard.getStructures().getFirst(); + assertEquals(StructureType.TOWN, storedStructure.getType()); + assertEquals(structure.getOwnerId(), storedStructure.getOwnerId()); + assertEquals(structure.getPos(), storedStructure.getPos()); + } + + @Test + void testUpgradeSettlementToTownNoStructureAtPos() { + PlayerRepresentation player = this.createPlayerRepresentation(); + + Structure structure = this.createBasicStructure(StructureType.SETTLEMENT); + gameBoard.getStructures().add(structure); + + BuildActionDTO dto = new BuildActionDTO( + StructureType.TOWN, + List.of(AxialPosition.of(-1, -1)) + ); + + gameBoard.upgradeSettlementToTown(player, dto); + + assertEquals(1, gameBoard.getStructures().size()); + assertEquals(structure, gameBoard.getStructures().getFirst()); + } + + @ParameterizedTest + @ArgumentsSource(FieldsAtPositionsProvider.class) + void testGetFields(List positions) throws GameBoard.NotAllFieldsFoundException { + List fields = gameBoard.getFieldsAt(positions); + + assertEquals(positions.size(), fields.size()); + for (AxialPosition pos : positions) { + assertTrue(fields.stream().anyMatch(field -> field.pos().equals(pos))); + } + } + + @Test + void testGetFieldsNotAllExist() { + AxialPosition pos = AxialPosition.of(5, 5); + + assertThrowsExactly(GameBoard.NotAllFieldsFoundException.class, () -> gameBoard.getFieldsAt(List.of(pos))); + } + + @ParameterizedTest + @ArgumentsSource(StructureTypesProvider.class) + void testGetStructureAtExists(StructureType structureType) { + Structure structure = this.createBasicStructure(structureType); + gameBoard.getStructures().add(structure); + + Structure foundStructure = gameBoard.getStructureAt(List.of(AxialPosition.of(1, 1))); + + assertNotNull(foundStructure); + assertEquals(structure, foundStructure); + } + + @Test + void testGetStructureAtNotExists() { + Structure foundStructure = gameBoard.getStructureAt(List.of(AxialPosition.of(1, 1))); + + assertNull(foundStructure); + } + + @Test + void testGetFieldsByNumberChip() { + List fields = List.of( + new Field(AxialPosition.of(1, 1), 2, ResourceType.BRICK), + new Field(AxialPosition.of(-1, -1), 11, ResourceType.WHEAT), + new Field(AxialPosition.of(2, 2), 5, ResourceType.SHEEP), + new Field(AxialPosition.of(-2, -2), 11, ResourceType.BRICK) + ); + + // 1 exists + List fieldsNumberChip2 = gameBoard.getFieldsByNumberChip(2); + assertEquals(1, fieldsNumberChip2.size()); + assertEquals(fields.getFirst(), fieldsNumberChip2.getFirst()); + + // 2 exist + List fieldsNumberChip11 = gameBoard.getFieldsByNumberChip(11); + assertEquals(2, fieldsNumberChip11.size()); + assertEquals(fields.get(1), fieldsNumberChip11.getFirst()); + assertEquals(fields.get(3), fieldsNumberChip11.getLast()); + + // doesn't exist + List fieldsNumberChip9 = gameBoard.getFieldsByNumberChip(9); + assertEquals(0, fieldsNumberChip9.size()); + } + + static class StructureTypesProvider implements ArgumentsProvider { + @Override + @NullMarked + public Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) { + return Arrays.stream(StructureType.values()) + .map(Arguments::of); + } + } + + static class FieldsAtPositionsProvider implements ArgumentsProvider { + @Override + @NullMarked + public Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) { + return Stream.of( + Arguments.of(List.of()), + Arguments.of(List.of( + AxialPosition.of(1, 1) + )), + Arguments.of(List.of( + AxialPosition.of(1, 1), + AxialPosition.of(-1, -1) + )) + ); + } + } + + private PlayerRepresentation createPlayerRepresentation() { + User user = User.builder() + .username("test") + .role(Role.GUEST) + .build(); + + Player playerBase = new Player(user, 0); + return new PlayerRepresentation(playerBase); + } + + private Structure createBasicStructure(StructureType structureType) { + return new Structure( + structureType, + List.of(AxialPosition.of(1, 1)), + 0, + Map.of() + ); + } + +} \ No newline at end of file diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/player/GamePlayersTest.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/player/GamePlayersTest.java new file mode 100644 index 0000000..a20822d --- /dev/null +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/player/GamePlayersTest.java @@ -0,0 +1,95 @@ +package de.hexfieldsstudio.hexfieldsdominion.game.player; + +import de.hexfieldsstudio.hexfieldsdominion.account.user.Role; +import de.hexfieldsstudio.hexfieldsdominion.account.user.User; +import de.hexfieldsstudio.hexfieldsdominion.config.AppConfig; +import de.hexfieldsstudio.hexfieldsdominion.lobby.Lobby; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class GamePlayersTest { + + private GamePlayers gamePlayers; + + private final User[] initialUsers = new User[]{ + User.builder().username("test").role(Role.GUEST).id(0).build(), + User.builder().username("test2").role(Role.GUEST).id(1).build(), + User.builder().username("test3").role(Role.GUEST).id(2).build() + }; + + @BeforeEach + void setupEach() { + AppConfig appConfig = mock(AppConfig.class); + when(appConfig.getHeartbeatCheckIntervalSeconds()).thenReturn(5L); + + Lobby lobby = spy(new Lobby(appConfig)); + when(lobby.getPlayers()).thenReturn(List.of( + new Player(initialUsers[0], initialUsers[0].getId()), + new Player(initialUsers[1], initialUsers[1].getId()), + new Player(initialUsers[2], initialUsers[2].getId()) + )); + + gamePlayers = new GamePlayers(lobby); + } + + @Test + void testRotateNextPlayer() { + List orderBefore = List.copyOf(gamePlayers.getPlayersTurnOrder()); + + gamePlayers.rotateNextPlayer(); + + assertEquals(orderBefore.getFirst(), gamePlayers.getPlayersTurnOrder().getLast()); + assertEquals(orderBefore.get(1), gamePlayers.getPlayersTurnOrder().getFirst()); + assertEquals(orderBefore.getLast(), gamePlayers.getPlayersTurnOrder().get(1)); + } + + @Test + void testGetPlayerCurrentTurn() { + assertEquals(gamePlayers.getPlayersTurnOrder().getFirst(), gamePlayers.getPlayerCurrentTurn()); + } + + @Test + void testIsPlayersTurn() { + User userTrue = initialUsers[gamePlayers.getPlayersTurnOrder().getFirst()]; + User userFalse = initialUsers[gamePlayers.getPlayersTurnOrder().get(1)]; + + assertTrue(gamePlayers.isPlayersTurn(userTrue)); + assertFalse(gamePlayers.isPlayersTurn(userFalse)); + } + + @Test + void testGetPlayerForUser() { + Optional playerOptionalNull = gamePlayers.getPlayerForUser(null); + Optional playerOptionalUnknown = gamePlayers.getPlayerForUser(User.builder().username("unknownUser").build()); + User existingUser = initialUsers[0]; + Optional playerOptionalExisting = gamePlayers.getPlayerForUser(existingUser); + + assertTrue(playerOptionalNull.isEmpty()); + assertTrue(playerOptionalUnknown.isEmpty()); + + assertTrue(playerOptionalExisting.isPresent()); + assertEquals(existingUser.getUsername(), playerOptionalExisting.get().getUsername()); + } + + @Test + void testGetPlayerById() { + Optional playerOptionalUnknown = gamePlayers.getPlayerById(10); + User existingUser = initialUsers[0]; + Optional playerOptionalExisting = gamePlayers.getPlayerById(existingUser.getId()); + + assertTrue(playerOptionalUnknown.isEmpty()); + + assertTrue(playerOptionalExisting.isPresent()); + assertEquals(existingUser.getUsername(), playerOptionalExisting.get().getUsername()); + } + +} diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/player/PlayerHueFactoryTest.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/player/PlayerHueFactoryTest.java new file mode 100644 index 0000000..29fd7c8 --- /dev/null +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/player/PlayerHueFactoryTest.java @@ -0,0 +1,44 @@ +package de.hexfieldsstudio.hexfieldsdominion.game.player; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PlayerHueFactoryTest { + + // different tests for short and long because it didn't work for short at some point + @Test + void testGenerateHueFromHashShortName() { + String[] usernames = new String[]{"t", "nw"}; + + this.runTestGeneratedHuesForUsernames(usernames); + } + + @Test + void testGenerateHueFromHashLongName() { + String[] usernames = new String[]{"test", "asdfghjk"}; + + this.runTestGeneratedHuesForUsernames(usernames); + } + + private void runTestGeneratedHuesForUsernames(String[] usernames) { + int[] generatedHues = new int[usernames.length]; + for (int i = 0; i < usernames.length; i++) { + generatedHues[i] = PlayerHueFactory.generateHueFromHash(usernames[i]); + } + + for (int i = 0; i < generatedHues.length; i++) { + assertTrue(generatedHues[i] >= 0); + assertTrue(generatedHues[i] < 360); + for (int j = 0; j < generatedHues.length; j++) { + //ignore same value + if (i == j) { + continue; + } + assertNotEquals(generatedHues[i], generatedHues[j]); + } + } + } + +} diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/player/PlayerRepresentationTest.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/player/PlayerRepresentationTest.java new file mode 100644 index 0000000..b3ecc9a --- /dev/null +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/player/PlayerRepresentationTest.java @@ -0,0 +1,52 @@ +package de.hexfieldsstudio.hexfieldsdominion.game.player; + +import de.hexfieldsstudio.hexfieldsdominion.account.user.Role; +import de.hexfieldsstudio.hexfieldsdominion.account.user.User; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PlayerRepresentationTest { + + @Test + void testNewPlayerRepresentation() { + User user = User.builder() + .username("test") + .role(Role.GUEST) + .build(); + + Player player = new Player(user, 0); + + PlayerRepresentation playerRepresentation = new PlayerRepresentation(player); + + assertEquals(player.getUsername(), playerRepresentation.getUsername()); + assertEquals(player.getId(), playerRepresentation.getPublicId()); + assertEquals(0, playerRepresentation.getPoints()); + assertTrue(playerRepresentation.getResources().isEmpty()); + + int expectedHue = PlayerHueFactory.generateHueFromHash(player.getUsername()); + assertEquals(expectedHue, playerRepresentation.getPlayerHue()); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 4, 11}) + void testAddPoints(int pointsToAdd) { + User user = User.builder() + .username("test") + .role(Role.GUEST) + .build(); + + Player player = new Player(user, 0); + + PlayerRepresentation playerRepresentation = new PlayerRepresentation(player); + playerRepresentation.setPoints(0); + + playerRepresentation.addPoints(pointsToAdd); + + assertEquals(pointsToAdd, playerRepresentation.getPoints()); + } + +} diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/player/PlayerTest.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/player/PlayerTest.java new file mode 100644 index 0000000..3c2fb7a --- /dev/null +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/player/PlayerTest.java @@ -0,0 +1,47 @@ +package de.hexfieldsstudio.hexfieldsdominion.game.player; + +import de.hexfieldsstudio.hexfieldsdominion.account.user.Role; +import de.hexfieldsstudio.hexfieldsdominion.account.user.User; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class PlayerTest { + + @Test + void testGetUsername() { + User user = User.builder() + .username("test") + .role(Role.GUEST) + .build(); + + Player player = new Player(user, 0); + + assertEquals(user.getUsername(), player.getUsername()); + } + + @Test + void testIsAccountTrue() { + User user = User.builder() + .username("test") + .role(Role.PLAYER) + .build(); + + Player player = new Player(user, 0); + + assertTrue(player.isAccount()); + } + + @Test + void testIsAccountFalse() { + User user = User.builder() + .username("test") + .role(Role.GUEST) + .build(); + + Player player = new Player(user, 0); + + assertFalse(player.isAccount()); + } + +} diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/trading/TradingHandlerTest.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/trading/TradingHandlerTest.java new file mode 100644 index 0000000..9b94a2d --- /dev/null +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/game/trading/TradingHandlerTest.java @@ -0,0 +1,508 @@ +package de.hexfieldsstudio.hexfieldsdominion.game.trading; + +import de.hexfieldsstudio.hexfieldsdominion.account.user.Role; +import de.hexfieldsstudio.hexfieldsdominion.account.user.User; +import de.hexfieldsstudio.hexfieldsdominion.error.BadRequestException; +import de.hexfieldsstudio.hexfieldsdominion.game.Match; +import de.hexfieldsstudio.hexfieldsdominion.game.dto.TradeBankDTO; +import de.hexfieldsstudio.hexfieldsdominion.game.dto.TradePlayerDTO; +import de.hexfieldsstudio.hexfieldsdominion.game.error.NotEnoughResourcesException; +import de.hexfieldsstudio.hexfieldsdominion.game.player.GamePlayers; +import de.hexfieldsstudio.hexfieldsdominion.game.player.Player; +import de.hexfieldsstudio.hexfieldsdominion.game.player.PlayerRepresentation; +import de.hexfieldsstudio.hexfieldsdominion.game.types.ResourceType; +import de.hexfieldsstudio.hexfieldsdominion.game.types.TradingStatus; +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.support.ParameterDeclarations; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import static java.util.Collections.emptyMap; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +public class TradingHandlerTest { + + private TradingHandler tradingHandler; + + @BeforeEach + public void setUp() { + tradingHandler = spy(new TradingHandler()); + } + + @ParameterizedTest + @ArgumentsSource(TradingStatusProvider.AllProvider.class) + void testHandleVerifyCalled(TradingStatus status) { + TradePlayerDtoObjects objects = TradePlayerDtoObjects.withPlayerTarget(tradingHandler, false, status); + User user = objects.getUser(); + Match match = objects.getMatch(); + TradePlayerDTO tradePlayerDTO = objects.getTradePlayerDTO(); + TradePlayerDTO existingTradeDto = objects.getExistingTradeDto(); + + // we don't want to call createTrade because of call count + tradingHandler.getPlayerTrades().put(0, new PlayerTrade( + existingTradeDto.getId(), + null, + existingTradeDto.getStatus(), + existingTradeDto.getTarget(), + 0, + existingTradeDto.getOffered(), + existingTradeDto.getRequested() + )); + + int expectedTimesCreate = status == TradingStatus.OFFERED ? 1 : 0; + int expectedTimesEdit = status == TradingStatus.CHANGED ? 1 : 0; + int expectedTimesAccept = status == TradingStatus.ACCEPTED ? 1 : 0; + int expectedTimesDeny = status == TradingStatus.DENIED ? 1 : 0; + int expectedTimesCancel = status == TradingStatus.CANCELLED ? 1 : 0; + + tradingHandler.handlePlayerTrade(user, match, tradePlayerDTO); + + verify(tradingHandler, times(expectedTimesCreate)).createTrade( + expectedTimesCreate == 0 ? any() : user, + expectedTimesCreate == 0 ? any() : match, + expectedTimesCreate == 0 ? any() : tradePlayerDTO + ); + verify(tradingHandler, times(expectedTimesEdit)).editTrade( + expectedTimesEdit == 0 ? any() : user, + expectedTimesEdit == 0 ? any() : match, + expectedTimesEdit == 0 ? any() : tradePlayerDTO + ); + verify(tradingHandler, times(expectedTimesAccept)).acceptTrade( + expectedTimesAccept == 0 ? any() : user, + expectedTimesAccept == 0 ? any() : match, + expectedTimesAccept == 0 ? any() : tradePlayerDTO + ); + verify(tradingHandler, times(expectedTimesDeny)).denyTrade( + expectedTimesDeny == 0 ? any() : tradePlayerDTO + ); + verify(tradingHandler, times(expectedTimesCancel)).cancelTrade( + expectedTimesCancel == 0 ? any() : tradePlayerDTO + ); + } + + @Test + void testCreateTradeNoPlayerForUser() { + User user = MockObjects.user(); + PlayerRepresentation player = MockObjects.player(user); + GamePlayers gamePlayers = MockObjects.gamePlayers(player); + Match match = MockObjects.match(gamePlayers); + + User userNoPlayer = MockObjects.user("testNoPlayer"); + + TradePlayerDTO tradePlayerDTO = mock(TradePlayerDTO.class); + + tradingHandler.createTrade(userNoPlayer, match, tradePlayerDTO); + + assertTrue(tradingHandler.getPlayerTrades().isEmpty()); + } + + @Test + void testCreateTrade() { + User user = MockObjects.user(); + PlayerRepresentation player = MockObjects.player(user); + GamePlayers gamePlayers = MockObjects.gamePlayers(player); + Match match = MockObjects.match(gamePlayers); + + TradePlayerDTO tradePlayerDTO = MockObjects.createTradeDto(player); + + tradingHandler.createTrade(user, match, tradePlayerDTO); + + assertFalse(tradingHandler.getPlayerTrades().isEmpty()); + PlayerTrade trade = tradingHandler.getPlayerTrades().get(0); + assertEquals(tradePlayerDTO.getStatus(), trade.getStatus()); + assertEquals(tradePlayerDTO.getTarget(), trade.getTarget()); + assertEquals(tradePlayerDTO.getOffered(), trade.getOffered()); + assertEquals(tradePlayerDTO.getRequested(), trade.getRequested()); + } + + @Test + void testCreateTradeWithExistingTrade() { + User user = MockObjects.user(); + PlayerRepresentation player = MockObjects.player(user); + GamePlayers gamePlayers = MockObjects.gamePlayers(player); + Match match = MockObjects.match(gamePlayers); + + TradePlayerDTO tradePlayerDTO = MockObjects.createTradeDto(player); + + tradingHandler.createTrade(user, match, tradePlayerDTO); + tradingHandler.createTrade(user, match, tradePlayerDTO); + + assertEquals(2, tradingHandler.getPlayerTrades().size()); + assertEquals(2, tradingHandler.getPlayerTrades().keySet().stream().distinct().count()); + PlayerTrade trade = tradingHandler.getPlayerTrades().get(1); + assertEquals(tradePlayerDTO.getStatus(), trade.getStatus()); + assertEquals(tradePlayerDTO.getTarget(), trade.getTarget()); + assertEquals(tradePlayerDTO.getOffered(), trade.getOffered()); + assertEquals(tradePlayerDTO.getRequested(), trade.getRequested()); + } + + @Test + void testEditTradeSingleTarget() { + TradePlayerDtoObjects objects = TradePlayerDtoObjects.withPlayerTarget(tradingHandler, true, TradingStatus.CHANGED); + User user = objects.getUser(); + + this.testEditTradeForObjects(objects, user, false, 0, false); + this.testEditTradeForObjects(objects, user, false, 1, true); + } + + @Test + void testEditTradeAllPlayersTarget() { + TradePlayerDtoObjects objects = TradePlayerDtoObjects.withAllPlayersTarget(tradingHandler, true, TradingStatus.CHANGED); + User user = objects.getUser(); + + this.testEditTradeForObjects(objects, user, true, 0, false); + this.testEditTradeForObjects(objects, user, false, 1, false); + } + + @Test + void testEditTradeAllPlayersTargetNoPlayerForUser() { + TradePlayerDtoObjects objects = TradePlayerDtoObjects.withAllPlayersTarget(tradingHandler, true, TradingStatus.CHANGED); + + User userNoPlayer = MockObjects.user("testNoPlayer"); + + this.testEditTradeForObjects(objects, userNoPlayer, true, 0, false); + this.testEditTradeForObjects(objects, userNoPlayer, false, 1, true); + } + + private void testEditTradeForObjects(TradePlayerDtoObjects objects, User editingUser, boolean expectValuesBefore, int tradeIdToCheck, boolean expectTradeDoesNotExist) { + Match match = objects.getMatch(); + + TradePlayerDTO tradePlayerDTO = objects.getTradePlayerDTO(); + when(tradePlayerDTO.getOffered()).thenReturn(Map.of(ResourceType.SHEEP, 2)); + when(tradePlayerDTO.getOffered()).thenReturn(Map.of(ResourceType.WHEAT, 1)); + + PlayerTrade tradeBefore = tradingHandler.getPlayerTrades().get(0); + TradingStatus statusBefore = tradeBefore.getStatus(); + Map offeredBefore = tradeBefore.getOffered(); + Map requestedBefore = tradeBefore.getRequested(); + + TradingStatus expectedStatus = expectValuesBefore ? statusBefore : tradePlayerDTO.getStatus(); + Map expectedOffered = expectValuesBefore ? offeredBefore : tradePlayerDTO.getOffered(); + Map expectedRequested = expectValuesBefore ? requestedBefore : tradePlayerDTO.getRequested(); + + tradingHandler.editTrade(editingUser, match, tradePlayerDTO); + + PlayerTrade tradeToCheck = tradingHandler.getPlayerTrades().get(tradeIdToCheck); + if (expectTradeDoesNotExist) { + assertNull(tradeToCheck); + return; + } + assertNotNull(tradeToCheck); + assertEquals(expectedStatus, tradeToCheck.getStatus()); + assertEquals(expectedOffered, tradeToCheck.getOffered()); + assertEquals(expectedRequested, tradeToCheck.getRequested()); + } + + //TODO: accept? Oder zu aufwendig für den Moment? + + @ParameterizedTest + @ArgumentsSource(TradingStatusProvider.AllProvider.class) + void testDenyTradeAllStatusSingleTarget(TradingStatus tradingStatus) { + TradePlayerDtoObjects objects = TradePlayerDtoObjects.withPlayerTarget(tradingHandler, true, TradingStatus.DENIED); + TradePlayerDTO tradePlayerDTO = objects.getTradePlayerDTO(); + + PlayerTrade trade = tradingHandler.getPlayerTrades().get(0); + trade.setStatus(tradingStatus); + + tradingHandler.denyTrade(tradePlayerDTO); + + assertEquals(TradingStatus.DENIED, trade.getStatus()); + } + + @ParameterizedTest + @ArgumentsSource(TradingStatusProvider.AllButOfferedProvider.class) + void testDenyTradeAllStatusWithoutOfferedAllPlayersTarget(TradingStatus tradingStatus) { + TradePlayerDtoObjects objects = TradePlayerDtoObjects.withAllPlayersTarget(tradingHandler, true, TradingStatus.DENIED); + TradePlayerDTO tradePlayerDTO = objects.getTradePlayerDTO(); + + PlayerTrade trade = tradingHandler.getPlayerTrades().get(0); + trade.setStatus(tradingStatus); + + tradingHandler.denyTrade(tradePlayerDTO); + + assertEquals(TradingStatus.DENIED, trade.getStatus()); + } + + @Test + void testDenyTradeOfferedToAllPlayers() { + TradePlayerDtoObjects objects = TradePlayerDtoObjects.withAllPlayersTarget(tradingHandler, true, TradingStatus.DENIED); + TradePlayerDTO tradePlayerDTO = objects.getTradePlayerDTO(); + + TradingStatus statusBefore = TradingStatus.OFFERED; + PlayerTrade trade = tradingHandler.getPlayerTrades().get(0); + trade.setStatus(statusBefore); + + tradingHandler.denyTrade(tradePlayerDTO); + + assertEquals(statusBefore, trade.getStatus()); + } + + @Test + void testCancelTrade() { + TradePlayerDtoObjects objects = TradePlayerDtoObjects.withPlayerTarget(tradingHandler, true, TradingStatus.CANCELLED); + TradePlayerDTO tradePlayerDTO = objects.getTradePlayerDTO(); + + tradingHandler.cancelTrade(tradePlayerDTO); + + assertEquals(TradingStatus.CANCELLED, tradingHandler.getPlayerTrades().get(0).getStatus()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testTradeBank(boolean alreadyOwnsRequestedResource) { + User user = MockObjects.user(); + PlayerRepresentation player = MockObjects.player(user); + GamePlayers gamePlayers = MockObjects.gamePlayers(player); + Match match = MockObjects.match(gamePlayers); + + Map resourcesBefore = alreadyOwnsRequestedResource ? Map.of( + ResourceType.WHEAT, 10, + ResourceType.WOOD, 10, + ResourceType.SHEEP, 10, + ResourceType.BRICK, 10 + ) : Map.of( + ResourceType.WHEAT, 10, + ResourceType.SHEEP, 10, + ResourceType.BRICK, 10 + ); + player.getResources().putAll(resourcesBefore); + + int amountRequested = 1; + int amountOffered = amountRequested * TradingHandler.GIVE_GET_RATIO; + TradeBankDTO tradeBankDTO = MockObjects.tradeBankDto(amountRequested, amountOffered); + + tradingHandler.tradeBank(user, match, tradeBankDTO); + + assertEquals(resourcesBefore.get(ResourceType.WHEAT), + player.getResources().get(ResourceType.WHEAT)); + if (alreadyOwnsRequestedResource) { + assertEquals(resourcesBefore.get(ResourceType.WOOD) + amountRequested, + player.getResources().get(ResourceType.WOOD)); + } else { + assertEquals(amountRequested, player.getResources().get(ResourceType.WOOD)); + } + assertEquals(resourcesBefore.get(ResourceType.SHEEP) - amountOffered, + player.getResources().get(ResourceType.SHEEP)); + assertEquals(resourcesBefore.get(ResourceType.BRICK), + player.getResources().get(ResourceType.BRICK)); + } + + @Test + void testTradeBankNoPlayerForUser() { + User user = MockObjects.user(); + PlayerRepresentation player = MockObjects.player(user); + GamePlayers gamePlayers = MockObjects.gamePlayers(player); + Match match = MockObjects.match(gamePlayers); + + Map resourcesBefore = Map.of( + ResourceType.WHEAT, 10, + ResourceType.WOOD, 10, + ResourceType.SHEEP, 10, + ResourceType.BRICK, 10 + ); + player.getResources().putAll(resourcesBefore); + + User userNoPlayer = MockObjects.user("testNoPlayer"); + + int amountRequested = 1; + int amountOffered = amountRequested * TradingHandler.GIVE_GET_RATIO; + TradeBankDTO tradeBankDTO = MockObjects.tradeBankDto(amountRequested, amountOffered); + + tradingHandler.tradeBank(userNoPlayer, match, tradeBankDTO); + + resourcesBefore.forEach((resource, amountBefore) -> { + assertEquals(amountBefore, player.getResources().get(resource)); + }); + } + + @Test + void testTradeBankInvalidRatio() { + User user = MockObjects.user(); + PlayerRepresentation player = MockObjects.player(user); + GamePlayers gamePlayers = MockObjects.gamePlayers(player); + Match match = MockObjects.match(gamePlayers); + + int amountRequested = 1; + int amountOffered = 2; + TradeBankDTO tradeBankDTO = MockObjects.tradeBankDto(amountRequested, amountOffered); + + assertThrowsExactly(BadRequestException.class, () -> tradingHandler.tradeBank(user, match, tradeBankDTO)); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testTradeBankNotEnoughResources(boolean offeredResourceAlreadyStored) { + User user = MockObjects.user(); + PlayerRepresentation player = MockObjects.player(user); + GamePlayers gamePlayers = MockObjects.gamePlayers(player); + Match match = MockObjects.match(gamePlayers); + + Map resourcesBefore = offeredResourceAlreadyStored ? Map.of( + ResourceType.WHEAT, 10, + ResourceType.WOOD, 10, + ResourceType.SHEEP, 0, + ResourceType.BRICK, 10 + ) : Map.of( + ResourceType.WHEAT, 10, + ResourceType.WOOD, 10, + ResourceType.BRICK, 10 + ); + player.getResources().putAll(resourcesBefore); + + int amountRequested = 1; + int amountOffered = amountRequested * TradingHandler.GIVE_GET_RATIO; + TradeBankDTO tradeBankDTO = MockObjects.tradeBankDto(amountRequested, amountOffered); + + assertThrowsExactly(NotEnoughResourcesException.class, () -> tradingHandler.tradeBank(user, match, tradeBankDTO)); + } + + @Test + void testClearTrades() { + PlayerTrade existingTrade = new PlayerTrade( + 0, + null, + TradingStatus.OFFERED, + TradingTarget.ofPlayer(0), + 0, + emptyMap(), + emptyMap() + ); + tradingHandler.getPlayerTrades().put(0, existingTrade); + + tradingHandler.clearTrades(); + + assertTrue(tradingHandler.getPlayerTrades().isEmpty()); + } + + static class TradingStatusProvider { + static class AllProvider implements ArgumentsProvider { + @Override + @NullMarked + public Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) { + return Arrays.stream(TradingStatus.values()) + .map(Arguments::of); + } + } + static class AllButOfferedProvider implements ArgumentsProvider { + @Override + @NullMarked + public Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) { + return Arrays.stream(TradingStatus.values()) + .filter(tradingStatus -> tradingStatus != TradingStatus.OFFERED) + .map(Arguments::of); + } + } + } + +} + +class MockObjects { + static User user() { + return user("test"); + } + static User user(String username) { + return User.builder() + .username(username) + .role(Role.PLAYER) + .build(); + } + + static PlayerRepresentation player(User user) { + return new PlayerRepresentation(new Player(user, 0)); + } + + static GamePlayers gamePlayers(PlayerRepresentation player) { + GamePlayers gamePlayers = mock(GamePlayers.class); + when(gamePlayers.getPlayerForUser(player.getPlayer().getUser())).thenReturn(Optional.of(player)); + return gamePlayers; + } + + static Match match(GamePlayers gamePlayers) { + Match match = mock(Match.class); + when(match.getPlayers()).thenReturn(gamePlayers); + return match; + } + + static TradeBankDTO tradeBankDto(int amountRequested, int amountOffered) { + TradeBankDTO tradeBankDTO = mock(TradeBankDTO.class); + when(tradeBankDTO.getResourceRequested()).thenReturn(ResourceType.WOOD); + when(tradeBankDTO.getAmountRequested()).thenReturn(amountRequested); + when(tradeBankDTO.getResourceOffered()).thenReturn(ResourceType.SHEEP); + when(tradeBankDTO.getAmountOffered()).thenReturn(amountOffered); + return tradeBankDTO; + } + + static TradePlayerDTO createTradeDto(PlayerRepresentation player) { + TradePlayerDTO tradePlayerDTO = mock(TradePlayerDTO.class); + when(tradePlayerDTO.getStatus()).thenReturn(TradingStatus.OFFERED); + when(tradePlayerDTO.getTarget()).thenReturn(TradingTarget.ofPlayer(player.getPublicId())); + when(tradePlayerDTO.getOffered()).thenReturn(emptyMap()); + when(tradePlayerDTO.getRequested()).thenReturn(emptyMap()); + return tradePlayerDTO; + } +} + +class TradePlayerDtoObjects { + private final User user; + private final Match match; + private final TradePlayerDTO tradePlayerDTO; + private final TradePlayerDTO existingTradeDto; + + public TradePlayerDtoObjects(TradingHandler tradingHandler, boolean createExistingTrade, TradingStatus tradingStatus, boolean allPlayers) { + this.user = MockObjects.user(); + PlayerRepresentation player = MockObjects.player(user); + GamePlayers gamePlayers = MockObjects.gamePlayers(player); + this.match = MockObjects.match(gamePlayers); + + this.existingTradeDto = new TradePlayerDTO( + 0, + TradingStatus.OFFERED, + allPlayers ? new TradingTarget(true, null) : TradingTarget.ofPlayer(player.getPublicId()), + emptyMap(), + emptyMap() + ); + if (createExistingTrade) { + tradingHandler.createTrade(user, match, existingTradeDto); + } + + this.tradePlayerDTO = mock(TradePlayerDTO.class); + when(tradePlayerDTO.getStatus()).thenReturn(tradingStatus); + when(tradePlayerDTO.getId()).thenReturn(existingTradeDto.getId()); + } + + public static TradePlayerDtoObjects withPlayerTarget(TradingHandler tradingHandler, boolean createExistingTrade, TradingStatus tradingStatus) { + return new TradePlayerDtoObjects(tradingHandler, createExistingTrade, tradingStatus, false); + } + + public static TradePlayerDtoObjects withAllPlayersTarget(TradingHandler tradingHandler, boolean createExistingTrade, TradingStatus tradingStatus) { + return new TradePlayerDtoObjects(tradingHandler, createExistingTrade, tradingStatus, true); + } + + public User getUser() { + return user; + } + + public Match getMatch() { + return match; + } + + public TradePlayerDTO getTradePlayerDTO() { + return tradePlayerDTO; + } + + public TradePlayerDTO getExistingTradeDto() { + return existingTradeDto; + } +} \ No newline at end of file diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyCodeGeneratorTest.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyCodeGeneratorTest.java new file mode 100644 index 0000000..2c3e20a --- /dev/null +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyCodeGeneratorTest.java @@ -0,0 +1,16 @@ +package de.hexfieldsstudio.hexfieldsdominion.lobby; + +import org.junit.jupiter.api.Test; + +import static de.hexfieldsstudio.hexfieldsdominion.TestUtils.assertLobbyCodeValid; + +public class LobbyCodeGeneratorTest { + + @Test + void testGenerateCode() { + String code = LobbyCodeGenerator.generateCode(); + + assertLobbyCodeValid(code); + } + +} diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyControllerTest.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyControllerTest.java new file mode 100644 index 0000000..9918927 --- /dev/null +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyControllerTest.java @@ -0,0 +1,248 @@ +package de.hexfieldsstudio.hexfieldsdominion.lobby; + +import de.hexfieldsstudio.hexfieldsdominion.account.AuthUtils; +import de.hexfieldsstudio.hexfieldsdominion.account.user.Role; +import de.hexfieldsstudio.hexfieldsdominion.account.user.User; +import de.hexfieldsstudio.hexfieldsdominion.game.Match; +import de.hexfieldsstudio.hexfieldsdominion.lobby.dto.CreateLobbyDTO; +import de.hexfieldsstudio.hexfieldsdominion.lobby.dto.HeartbeatDTO; +import de.hexfieldsstudio.hexfieldsdominion.lobby.error.LobbyNotFoundException; +import de.hexfieldsstudio.hexfieldsdominion.lobby.heartbeat.HeartbeatHandler; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Map; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class LobbyControllerTest { + + private static final String LOBBY_CODE = "ABC123D"; + + @InjectMocks + private LobbyController lobbyController; + + @Mock + private LobbyManager lobbyManager; + + @Test + void testCreateLobbyWithDto() throws Exception { + User user = User.builder() + .username("testuser") + .role(Role.GUEST) + .build(); + + CreateLobbyDTO dto = new CreateLobbyDTO(new String[2]); + + when(lobbyManager.createLobby(dto.configs(), user.getUsername())).thenReturn(LOBBY_CODE); + + try (MockedStatic authUtils = mockStatic(AuthUtils.class)) { + authUtils.when(AuthUtils::getAuthenticatedUser).thenReturn(user); + + ResponseEntity> responseEntity = lobbyController.createLobby(dto); + + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + Map response = responseEntity.getBody(); + assertNotNull(response); + assertEquals(1, response.size()); + assertTrue(response.containsKey("lobbyCode")); + assertEquals(LOBBY_CODE, response.get("lobbyCode")); + } + } + + @Test + void testCreateLobbyWithoutDto() throws Exception { + User user = User.builder() + .username("testuser") + .role(Role.GUEST) + .build(); + + when(lobbyManager.createLobby(new String[0], user.getUsername())).thenReturn(LOBBY_CODE); + + try (MockedStatic authUtils = mockStatic(AuthUtils.class)) { + authUtils.when(AuthUtils::getAuthenticatedUser).thenReturn(user); + + ResponseEntity> responseEntity = lobbyController.createLobby(null); + + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + Map response = responseEntity.getBody(); + assertNotNull(response); + assertEquals(1, response.size()); + assertTrue(response.containsKey("lobbyCode")); + assertEquals(LOBBY_CODE, response.get("lobbyCode")); + } + } + + @Test + void testCreateLobbyException() throws Exception { + User user = User.builder() + .username("testuser") + .role(Role.GUEST) + .build(); + + String exceptionMessage = "some msg"; + + when(lobbyManager.createLobby(new String[0], user.getUsername())).thenThrow(new Exception(exceptionMessage)); + + try (MockedStatic authUtils = mockStatic(AuthUtils.class)) { + authUtils.when(AuthUtils::getAuthenticatedUser).thenReturn(user); + + ResponseEntity> responseEntity = lobbyController.createLobby(null); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, responseEntity.getStatusCode()); + Map response = responseEntity.getBody(); + assertNotNull(response); + assertEquals(1, response.size()); + assertTrue(response.containsKey("error")); + assertEquals(exceptionMessage, response.get("error")); + } + } + + @Test + void testJoinLobbyLobbyExists() { + User user = User.builder() + .username("testuser") + .role(Role.GUEST) + .build(); + + LobbyManager.JoinedLobbyResponse expectedResponse = mock(LobbyManager.JoinedLobbyResponse.class); + + when(lobbyManager.joinLobby(LOBBY_CODE, user)).thenReturn(expectedResponse); + + try (MockedStatic authUtils = mockStatic(AuthUtils.class)) { + authUtils.when(AuthUtils::getAuthenticatedUser).thenReturn(user); + + ResponseEntity responseEntity = lobbyController.joinLobby(LOBBY_CODE); + + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + assertEquals(expectedResponse, responseEntity.getBody()); + } + } + + @Test + void testJoinLobbyLobbyDoesNotExist() { + User user = User.builder() + .username("testuser") + .role(Role.GUEST) + .build(); + + when(lobbyManager.joinLobby(LOBBY_CODE, user)).thenThrow(new LobbyNotFoundException(LOBBY_CODE)); + + try (MockedStatic authUtils = mockStatic(AuthUtils.class)) { + authUtils.when(AuthUtils::getAuthenticatedUser).thenReturn(user); + + assertThrowsExactly(LobbyNotFoundException.class, () -> lobbyController.joinLobby(LOBBY_CODE)); + } + } + + @Test + void testDoesLobbyWithCodeExistTrue() { + HttpServletResponse response = mock(HttpServletResponse.class); + Lobby existingLobby = mock(Lobby.class); + + when(lobbyManager.findOccupiedLobbyOrThrow(LOBBY_CODE)).thenReturn(existingLobby); + + lobbyController.doesLobbyWithCodeExist(LOBBY_CODE, response); + + verify(response, times(1)).setStatus(HttpServletResponse.SC_OK); + verify(response, times(1)).setStatus(anyInt()); + } + + @Test + void testDoesLobbyWithCodeExistFalse() { + HttpServletResponse response = mock(HttpServletResponse.class); + + when(lobbyManager.findOccupiedLobbyOrThrow(LOBBY_CODE)).thenThrow(new LobbyNotFoundException(LOBBY_CODE)); + + assertThrowsExactly(LobbyNotFoundException.class, () -> lobbyController.doesLobbyWithCodeExist(LOBBY_CODE, response)); + } + + @Test + void testHeartbeatLobbyExists() { + HttpServletResponse response = mock(HttpServletResponse.class); + Lobby existingLobby = mock(Lobby.class); + HeartbeatHandler heartbeatHandler = mock(HeartbeatHandler.class); + when(existingLobby.getHeartbeatHandler()).thenReturn(heartbeatHandler); + + HeartbeatDTO dto = new HeartbeatDTO(0); + + when(lobbyManager.findOccupiedLobbyOrThrow(LOBBY_CODE)).thenReturn(existingLobby); + + lobbyController.heartbeat(LOBBY_CODE, dto, response); + + verify(response, times(1)).setStatus(HttpServletResponse.SC_OK); + verify(response, times(1)).setStatus(anyInt()); + } + + @Test + void testHeartbeatLobbyDoesNotExist() { + HttpServletResponse response = mock(HttpServletResponse.class); + + HeartbeatDTO dto = new HeartbeatDTO(0); + + when(lobbyManager.findOccupiedLobbyOrThrow(LOBBY_CODE)).thenThrow(new LobbyNotFoundException(LOBBY_CODE)); + + assertThrowsExactly(LobbyNotFoundException.class, () -> lobbyController.heartbeat(LOBBY_CODE, dto, response)); + } + + @Test + void testLobbyEvents() { + User user = User.builder() + .username("testuser") + .role(Role.GUEST) + .build(); + + SseEmitter expectedEmitter = mock(SseEmitter.class); + + try (MockedStatic authUtils = mockStatic(AuthUtils.class)) { + authUtils.when(AuthUtils::getAuthenticatedUser).thenReturn(user); + when(lobbyManager.subscribe(LOBBY_CODE, user.getUsername())).thenReturn(expectedEmitter); + + SseEmitter createdEmitter = lobbyController.lobbyEvents(LOBBY_CODE); + + assertEquals(expectedEmitter, createdEmitter); + } + } + + @Test + void testMatchLobbyExists() { + User user = User.builder() + .username("testuser") + .role(Role.GUEST) + .build(); + + Lobby existingLobby = mock(Lobby.class); + Match match = mock(Match.class); + when(match.getUuid()).thenReturn(UUID.randomUUID()); + + when(lobbyManager.findOccupiedLobbyOrThrow(LOBBY_CODE)).thenReturn(existingLobby); + when(lobbyManager.createMatchForLobby(existingLobby, user, LobbyController.BOARD_RADIUS)).thenReturn(match); + + try (MockedStatic authUtils = mockStatic(AuthUtils.class)) { + authUtils.when(AuthUtils::getAuthenticatedUser).thenReturn(user); + + LobbyManager.CreatedMatchResponse response = lobbyController.match(LOBBY_CODE); + + assertEquals(match.getUuid().toString(), response.matchUUID()); + } + } + + @Test + void testMatchLobbyDoesNotExist() { + when(lobbyManager.findOccupiedLobbyOrThrow(LOBBY_CODE)).thenThrow(new LobbyNotFoundException(LOBBY_CODE)); + + assertThrowsExactly(LobbyNotFoundException.class, () -> lobbyController.match(LOBBY_CODE)); + } + +} diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyManagerTest.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyManagerTest.java new file mode 100644 index 0000000..752d679 --- /dev/null +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyManagerTest.java @@ -0,0 +1,239 @@ +package de.hexfieldsstudio.hexfieldsdominion.lobby; + +import de.hexfieldsstudio.hexfieldsdominion.account.user.Role; +import de.hexfieldsstudio.hexfieldsdominion.account.user.User; +import de.hexfieldsstudio.hexfieldsdominion.config.AppConfig; +import de.hexfieldsstudio.hexfieldsdominion.game.Match; +import de.hexfieldsstudio.hexfieldsdominion.game.error.MatchNotFoundException; +import de.hexfieldsstudio.hexfieldsdominion.game.error.TooLittleSpaceException; +import de.hexfieldsstudio.hexfieldsdominion.lobby.error.InvalidRadiusException; +import de.hexfieldsstudio.hexfieldsdominion.lobby.error.LobbyNotFoundException; +import de.hexfieldsstudio.hexfieldsdominion.lobby.error.NotOwnerOfLobbyException; +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.support.ParameterDeclarations; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.stream.Stream; + +import static de.hexfieldsstudio.hexfieldsdominion.TestUtils.assertLobbyCodeValid; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class LobbyManagerTest { + + private static final int INITIAL_CAPACITY = 10; + + private AppConfig appConfig; + + private LobbyManager lobbyManager; + + @BeforeEach + void setupEach() { + appConfig = mock(AppConfig.class); + when(appConfig.getInitialCapacity()).thenReturn(INITIAL_CAPACITY); + + lobbyManager = new LobbyManager(appConfig); + } + + @Test + void testCreateLobbySuccessManyLobbiesFree() throws Exception { + this.testCreateLobbySuccess(); + } + + @Test + void testCreateLobbySuccessOneLobbyFree() throws Exception { + when(appConfig.getInitialCapacity()).thenReturn(1); + lobbyManager = new LobbyManager(appConfig); + + this.testCreateLobbySuccess(); + } + + private void testCreateLobbySuccess() throws Exception { + String lobbyCode = lobbyManager.createLobby(new String[0], "someone"); + + assertLobbyCodeValid(lobbyCode); + } + + @Test + void testCreateLobbyFailAllLobbiesOccupied() { + when(appConfig.getInitialCapacity()).thenReturn(0); + lobbyManager = new LobbyManager(appConfig); + + Exception exception = assertThrowsExactly(Exception.class, () -> lobbyManager.createLobby(new String[0], "someone")); + assertEquals("Server Capacity has been reached. Could not create lobby.", exception.getMessage()); + } + + @Test + void testJoinLobbySuccess() throws Exception { + this.createLobbyWithUser((user, lobbyCode) -> { + LobbyManager.JoinedLobbyResponse response = lobbyManager.joinLobby(lobbyCode, user); + + assertTrue(response.isLobbyOwner()); + assertEquals(user.getUsername(), response.createdPlayer().username()); + }); + } + + @ParameterizedTest + @ArgumentsSource(UnknownLobbyCodesProvider.class) + void testJoinLobbyInvalidLobbyCode(String lobbyCode) { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + + assertThrowsExactly(LobbyNotFoundException.class, () -> lobbyManager.joinLobby(lobbyCode, user)); + } + + @Test + void testFindOccupiedLobbyOrThrowFound() throws Exception { + this.createLobbyWithUser((user, lobbyCode) -> { + Lobby foundLobby = lobbyManager.findOccupiedLobbyOrThrow(lobbyCode); + + assertNotNull(foundLobby); + assertEquals(lobbyCode, foundLobby.getLobbyCode()); + assertTrue(foundLobby.isOwner(user.getUsername())); + }); + } + + @ParameterizedTest + @ArgumentsSource(UnknownLobbyCodesProvider.class) + void testFindOccupiedLobbyOrThrowNotFound(String lobbyCode) { + assertThrowsExactly(LobbyNotFoundException.class, () -> lobbyManager.findOccupiedLobbyOrThrow(lobbyCode)); + } + + @ParameterizedTest + @ValueSource(ints = {3, 4, 5, 6}) + void testCreateMatchForLobbySuccess(int boardRadius) throws Exception { + this.createLobbyWithUser((user, lobbyCode) -> { + lobbyManager.joinLobby(lobbyCode, user); + lobbyManager.subscribe(lobbyCode, user.getUsername()); + Lobby lobby = lobbyManager.findOccupiedLobbyOrThrow(lobbyCode); + + try { + Match match = lobbyManager.createMatchForLobby(lobby, user, boardRadius); + + assertNotNull(match); + assertNotNull(match.getUuid()); + assertEquals(match, lobby.getMatch()); + } catch (IllegalArgumentException | TooLittleSpaceException e) { + //TODO: remove try/catch when exception is not sometimes thrown for boardRadius 4, 5, 6 anymore (looks like an error in Match(): StructureFactory.randomlyBuildInitialStructures) + } + }); + } + + @Test + void testCreateMatchForLobbyFailNotOwnerOfLobby() throws Exception { + User userNotOwner = User.builder() + .username("notOwner") + .role(Role.GUEST) + .build(); + + this.createLobbyWithUser((userOwner, lobbyCode) -> { + lobbyManager.joinLobby(lobbyCode, userOwner); + lobbyManager.subscribe(lobbyCode, userOwner.getUsername()); + lobbyManager.joinLobby(lobbyCode, userNotOwner); + lobbyManager.subscribe(lobbyCode, userNotOwner.getUsername()); + Lobby lobby = lobbyManager.findOccupiedLobbyOrThrow(lobbyCode); + + assertThrowsExactly(NotOwnerOfLobbyException.class, () -> lobbyManager.createMatchForLobby(lobby, userNotOwner, LobbyController.BOARD_RADIUS)); + }); + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 7, 8}) + void testCreateMatchForLobbyFailInvalidBoardRadius(int boardRadius) throws Exception { + this.createLobbyWithUser((user, lobbyCode) -> { + lobbyManager.joinLobby(lobbyCode, user); + lobbyManager.subscribe(lobbyCode, user.getUsername()); + Lobby lobby = lobbyManager.findOccupiedLobbyOrThrow(lobbyCode); + + assertThrowsExactly(InvalidRadiusException.class, () -> lobbyManager.createMatchForLobby(lobby, user, boardRadius)); + }); + } + + @Test + void testFindLobbyByMatchSuccess() throws Exception { + this.createLobbyWithUser((user, lobbyCode) -> { + lobbyManager.joinLobby(lobbyCode, user); + lobbyManager.subscribe(lobbyCode, user.getUsername()); + Lobby lobby = lobbyManager.findOccupiedLobbyOrThrow(lobbyCode); + try { + Match match = lobbyManager.createMatchForLobby(lobby, user, LobbyController.BOARD_RADIUS); + + Lobby lobbyFound = lobbyManager.findLobbyByMatch(match.getUuid()); + + assertEquals(lobby, lobbyFound); + } catch (TooLittleSpaceException e) { + //TODO: remove try/catch when exception is not sometimes thrown + } + }); + } + + @Test + void testFindLobbyByMatchFail() { + assertThrowsExactly(MatchNotFoundException.class, () -> lobbyManager.findLobbyByMatch(UUID.randomUUID())); + } + + @Test + void testSubscribeLobbyFound() throws Exception { + this.createLobbyWithUser((user, lobbyCode) -> { + SseEmitter emitter = lobbyManager.subscribe(lobbyCode, user.getUsername()); + + assertNotNull(emitter); + }); + } + + @ParameterizedTest + @ArgumentsSource(UnknownLobbyCodesProvider.class) + void testSubscribeLobbyNotFound(String lobbyCode) { + SseEmitter emitter = lobbyManager.subscribe(lobbyCode, "someone"); + + assertNotNull(emitter); + } + + @Test + void testOnUnsubscribeLobbyExists() throws Exception { + this.createLobbyWithUser((user, lobbyCode) -> { + lobbyManager.joinLobby(lobbyCode, user); + lobbyManager.subscribe(lobbyCode, user.getUsername()); + Lobby lobby = lobbyManager.findOccupiedLobbyOrThrow(lobbyCode); + + lobbyManager.onUnsubscribe(lobbyCode, user.getUsername()); + + assertFalse(lobby.getPlayers().stream().anyMatch(player -> user.getUsername().equals(player.getUsername()))); + }); + } + + private void createLobbyWithUser(BiConsumer consumer) throws Exception { + User user = User.builder() + .username("someone") + .role(Role.GUEST) + .build(); + String lobbyCode = lobbyManager.createLobby(new String[0], user.getUsername()); + consumer.accept(user, lobbyCode); + } + + static class UnknownLobbyCodesProvider implements ArgumentsProvider { + @Override + @NullMarked + public Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) { + return Stream.of("", " ", "x", "ABS68J9") + .map(Arguments::of); + } + } + +} diff --git a/src/test/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyTest.java b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyTest.java new file mode 100644 index 0000000..24bbb9c --- /dev/null +++ b/src/test/java/de/hexfieldsstudio/hexfieldsdominion/lobby/LobbyTest.java @@ -0,0 +1,159 @@ +package de.hexfieldsstudio.hexfieldsdominion.lobby; + +import de.hexfieldsstudio.hexfieldsdominion.account.user.Role; +import de.hexfieldsstudio.hexfieldsdominion.account.user.User; +import de.hexfieldsstudio.hexfieldsdominion.config.AppConfig; +import de.hexfieldsstudio.hexfieldsdominion.game.player.Player; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.when; + +public class LobbyTest { + + private Lobby lobby; + + private LobbyManager lobbyManager; + + @BeforeEach + void setupEach() { + AppConfig appConfig = mock(AppConfig.class); + when(appConfig.getHeartbeatCheckIntervalSeconds()).thenReturn(5L); + + lobby = new Lobby(appConfig); + + lobbyManager = mock(LobbyManager.class); + } + + @Test + void testAddPlayerAlreadyExists() { + Player player = this.createPlayer(); + + lobby.getPlayers().add(player); + + Player storedPlayer = lobby.addPlayer(player.getUser(), lobbyManager); + + assertEquals(player, storedPlayer); + } + + @Test + void testAddPlayerNotExistsGuest() { + this.testAddPlayerNotExists(Role.GUEST); + + assertFalse(lobby.isHasAccountPlayer()); + } + + @Test + void testAddPlayerNotExistsAccount() { + this.testAddPlayerNotExists(Role.PLAYER); + + assertTrue(lobby.isHasAccountPlayer()); + } + + private void testAddPlayerNotExists(Role role) { + User user = User.builder() + .username("test") + .role(role) + .build(); + + Player storedPlayer = lobby.addPlayer(user, lobbyManager); + + assertEquals(user.getUsername(), storedPlayer.getUsername()); + Optional playerInListOptional = lobby.getPlayers().stream().filter(player -> player.getUsername().equals(user.getUsername())).findFirst(); + assertTrue(playerInListOptional.isPresent()); + assertEquals(storedPlayer.getId(), playerInListOptional.get().getId()); + } + + @Test + void testRemovePlayerUsernameExists() { + Player player = this.createPlayer(); + + lobby.getPlayers().add(player); + + lobby.removePlayer(player.getUsername()); + + assertTrue(lobby.getPlayers().isEmpty()); + } + + @Test + void testRemovePlayerUsernameNotExists() { + lobby.removePlayer("test"); + + assertTrue(lobby.getPlayers().isEmpty()); + } + + @Test + void testRemovePlayerIdExists() { + Player player = this.createPlayer(); + + lobby.getPlayers().add(player); + + lobby.removePlayer(player.getId()); + + assertTrue(lobby.getPlayers().isEmpty()); + } + + @Test + void testRemovePlayerIdNotExists() { + Player player = this.createPlayer(); + + lobby.getPlayers().add(player); + + lobby.removePlayer(1); + + assertEquals(1, lobby.getPlayers().size()); + assertEquals(player, lobby.getPlayers().getFirst()); + } + + @Test + void testIsOwnerTrue() { + Player player = this.createPlayer(); + + lobby.getPlayers().add(player); + lobby.setOwner(player.getUsername()); + + assertTrue(lobby.isOwner(player.getUsername())); + } + + @Test + void testIsOwnerFalse() { + Player player = this.createPlayer(); + + lobby.getPlayers().add(player); + lobby.setOwner("other"); + + assertFalse(lobby.isOwner(player.getUsername())); + } + + @Test + void testOnNoHeartbeatPlayerExists() { + Player player = this.createPlayer(); + + lobby.getPlayers().add(player); + + lobby.onNoHeartbeat(lobby, player.getId()); + + assertTrue(lobby.getPlayers().isEmpty()); + } + + @Test + void testOnNoHeartbeatPlayerNotExists() { + lobby.onNoHeartbeat(lobby, 0); + + assertTrue(lobby.getPlayers().isEmpty()); + } + + private Player createPlayer() { + User user = User.builder() + .username("test") + .role(Role.GUEST) + .build(); + + return new Player(user, 0); + } + +}