diff --git a/backend/requests/blackjack.http b/backend/requests/blackjack.http new file mode 100644 index 0000000..ea20985 --- /dev/null +++ b/backend/requests/blackjack.http @@ -0,0 +1,7 @@ +POST http://localhost:8080/blackjack/start +Authorization: Bearer {{token}} +Content-Type: application/json + +{ + "betAmount": 1.01 +} diff --git a/backend/requests/getBearerToken.http b/backend/requests/getBearerToken.http index afdea20..791c3a2 100644 --- a/backend/requests/getBearerToken.http +++ b/backend/requests/getBearerToken.http @@ -1,6 +1,6 @@ POST http://localhost:9090/realms/LF12/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded -grant_type=password&client_id=lf12&username=lf12_test_user&password=secret +grant_type=password&client_id=lf12&username=lf12_test_user&password=secret&scope=openid > {% client.global.set("token", response.body.access_token); %} \ No newline at end of file diff --git a/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameController.java b/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameController.java new file mode 100644 index 0000000..0aab2cb --- /dev/null +++ b/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameController.java @@ -0,0 +1,57 @@ +package de.szut.casino.blackjack; + +import de.szut.casino.blackjack.dto.CreateBlackJackGameDto; +import de.szut.casino.user.UserEntity; +import de.szut.casino.user.UserService; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Slf4j +@RestController +public class BlackJackGameController { + + private final UserService userService; + private final BlackJackService blackJackService; + + public BlackJackGameController(UserService userService, BlackJackService blackJackService) { + this.blackJackService = blackJackService; + this.userService = userService; + } + + @PostMapping("/blackjack/start") + public ResponseEntity createBlackJackGame(@RequestBody @Valid CreateBlackJackGameDto createBlackJackGameDto, @RequestHeader("Authorization") String token) { + Optional optionalUser = userService.getCurrentUser(token); + + if (optionalUser.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + UserEntity user = optionalUser.get(); + BigDecimal balance = user.getBalance(); + BigDecimal betAmount = createBlackJackGameDto.getBetAmount(); + + if (betAmount.compareTo(BigDecimal.ZERO) <= 0) { + Map errorResponse = new HashMap<>(); + errorResponse.put("error", "Invalid bet amount"); + return ResponseEntity.badRequest().body(errorResponse); + } + + if (betAmount.compareTo(balance) > 0) { + Map errorResponse = new HashMap<>(); + errorResponse.put("error", "Insufficient funds"); + return ResponseEntity.badRequest().body(errorResponse); + } + + return ResponseEntity.ok(blackJackService.createBlackJackGame(user, betAmount)); + } +} diff --git a/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameEntity.java b/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameEntity.java new file mode 100644 index 0000000..e3380a7 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameEntity.java @@ -0,0 +1,46 @@ +package de.szut.casino.blackjack; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import de.szut.casino.user.UserEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class BlackJackGameEntity { + @Id + @GeneratedValue + private Long id; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + @JsonIgnore + private UserEntity user; + + // Expose UserID to JSON output + public Long getUserId() { + return user != null ? user.getId() : null; + } + + private String state; + private BigDecimal bet; + + @OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List playerCards = new ArrayList<>(); + + @OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List dealerCards = new ArrayList<>(); +} diff --git a/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameRepository.java b/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameRepository.java new file mode 100644 index 0000000..d25a180 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameRepository.java @@ -0,0 +1,12 @@ +package de.szut.casino.blackjack; + +import de.szut.casino.user.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public interface BlackJackGameRepository extends JpaRepository { +} diff --git a/backend/src/main/java/de/szut/casino/blackjack/BlackJackService.java b/backend/src/main/java/de/szut/casino/blackjack/BlackJackService.java new file mode 100644 index 0000000..ba5e7d1 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/blackjack/BlackJackService.java @@ -0,0 +1,50 @@ +package de.szut.casino.blackjack; + +import de.szut.casino.user.UserEntity; +import de.szut.casino.user.UserRepository; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.Random; + +@Service +public class BlackJackService { + private final BlackJackGameRepository blackJackGameRepository; + private final UserRepository userRepository; + + public BlackJackService(BlackJackGameRepository blackJackGameRepository, UserRepository userRepository) { + this.blackJackGameRepository = blackJackGameRepository; + this.userRepository = userRepository; + } + + private final Random random = new Random(); + + public BlackJackGameEntity createBlackJackGame(UserEntity user, BigDecimal betAmount) { + BlackJackGameEntity game = new BlackJackGameEntity(); + game.setUser(user); + game.setBet(betAmount); + + for (int i = 0; i < 2; i++) { + CardEntity playerCard = createRandomCard(game); + game.getPlayerCards().add(playerCard); + } + + CardEntity dealerCard = createRandomCard(game); + game.getDealerCards().add(dealerCard); + + user.setBalance(user.getBalance().subtract(betAmount)); + + userRepository.save(user); + blackJackGameRepository.save(game); + + return game; + } + + private CardEntity createRandomCard(BlackJackGameEntity game) { + CardEntity card = new CardEntity(); + card.setGame(game); + card.setSuit(Suit.values()[random.nextInt(Suit.values().length)]); + card.setRank(Rank.values()[random.nextInt(Rank.values().length)]); + return card; + } +} diff --git a/backend/src/main/java/de/szut/casino/blackjack/CardEntity.java b/backend/src/main/java/de/szut/casino/blackjack/CardEntity.java new file mode 100644 index 0000000..8026d72 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/blackjack/CardEntity.java @@ -0,0 +1,33 @@ +package de.szut.casino.blackjack; + + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class CardEntity { + @Id + @GeneratedValue + @JsonIgnore + private Long id; + + @ManyToOne + @JoinColumn(name = "game_id", nullable = false) + @JsonBackReference + private BlackJackGameEntity game; + + @Enumerated(EnumType.STRING) + private Suit suit; + + @Enumerated(EnumType.STRING) + private Rank rank; +} diff --git a/backend/src/main/java/de/szut/casino/blackjack/Rank.java b/backend/src/main/java/de/szut/casino/blackjack/Rank.java new file mode 100644 index 0000000..8f9a3b8 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/blackjack/Rank.java @@ -0,0 +1,31 @@ +package de.szut.casino.blackjack; + +import lombok.Getter; + +@Getter +public enum Rank { + TWO("2", "Two", 2), + THREE("3", "Three", 3), + FOUR("4", "Four", 4), + FIVE("5", "Five", 5), + SIX("6", "Six", 6), + SEVEN("7", "Seven", 7), + EIGHT("8", "Eight", 8), + NINE("9", "Nine", 9), + TEN("10", "Ten", 10), + JACK("J", "Jack", 10), + QUEEN("Q", "Queen", 10), + KING("K", "King", 10), + ACE("A", "Ace", 11); + + private final String symbol; + private final String displayName; + private final int value; + + Rank(String symbol, String displayName, int value) { + this.symbol = symbol; + this.displayName = displayName; + this.value = value; + } + +} diff --git a/backend/src/main/java/de/szut/casino/blackjack/Suit.java b/backend/src/main/java/de/szut/casino/blackjack/Suit.java new file mode 100644 index 0000000..8ee80a9 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/blackjack/Suit.java @@ -0,0 +1,20 @@ +package de.szut.casino.blackjack; + +import lombok.Getter; + +@Getter +public enum Suit { + HEARTS("H", "Hearts"), + DIAMONDS("D", "Diamonds"), + CLUBS("C", "Clubs"), + SPADES("S", "Spades"); + + private final String symbol; + private final String displayName; + + Suit(String symbol, String displayName) { + this.symbol = symbol; + this.displayName = displayName; + } + +} diff --git a/backend/src/main/java/de/szut/casino/blackjack/dto/CreateBlackJackGameDto.java b/backend/src/main/java/de/szut/casino/blackjack/dto/CreateBlackJackGameDto.java new file mode 100644 index 0000000..e5b0c97 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/blackjack/dto/CreateBlackJackGameDto.java @@ -0,0 +1,16 @@ +package de.szut.casino.blackjack.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class CreateBlackJackGameDto { + private BigDecimal betAmount; +} diff --git a/backend/src/main/java/de/szut/casino/user/UserController.java b/backend/src/main/java/de/szut/casino/user/UserController.java index 4d232ac..3750c8b 100644 --- a/backend/src/main/java/de/szut/casino/user/UserController.java +++ b/backend/src/main/java/de/szut/casino/user/UserController.java @@ -44,7 +44,7 @@ public class UserController { @GetMapping("/user") public ResponseEntity getCurrentUser(@RequestHeader("Authorization") String token) { - GetUserDto userData = userService.getCurrentUser(token); + GetUserDto userData = userService.getCurrentUserAsDto(token); if (userData == null) { return ResponseEntity.notFound().build(); diff --git a/backend/src/main/java/de/szut/casino/user/UserService.java b/backend/src/main/java/de/szut/casino/user/UserService.java index 724962e..36569e6 100644 --- a/backend/src/main/java/de/szut/casino/user/UserService.java +++ b/backend/src/main/java/de/szut/casino/user/UserService.java @@ -38,7 +38,7 @@ public class UserService { return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null); } - public GetUserDto getCurrentUser(String token) { + public GetUserDto getCurrentUserAsDto(String token) { KeycloakUserDto userData = getKeycloakUserInfo(token); if (userData == null) { @@ -47,7 +47,15 @@ public class UserService { Optional user = this.userRepository.findOneByKeycloakId(userData.getSub()); return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null); + } + public Optional getCurrentUser(String token) { + KeycloakUserDto userData = getKeycloakUserInfo(token); + + if (userData == null) { + return Optional.empty(); + } + return this.userRepository.findOneByKeycloakId(userData.getSub()); } private KeycloakUserDto getKeycloakUserInfo(String token) { diff --git a/backend/src/test/java/de/szut/casino/user/UserControllerTest.java b/backend/src/test/java/de/szut/casino/user/UserControllerTest.java deleted file mode 100644 index 2addb43..0000000 --- a/backend/src/test/java/de/szut/casino/user/UserControllerTest.java +++ /dev/null @@ -1,122 +0,0 @@ -package de.szut.casino.user; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import de.szut.casino.user.dto.CreateUserDto; -import de.szut.casino.user.dto.GetUserDto; - -@WebMvcTest(UserController.class) -@AutoConfigureMockMvc(addFilters = false) -public class UserControllerTest { - - @Autowired - private MockMvc mockMvc; - - @Autowired - private ObjectMapper objectMapper; - - @MockBean - private UserService userService; - - private GetUserDto getUserDto; - private CreateUserDto createUserDto; - private UserEntity testUser; - private final String TEST_ID = "test-id-123"; - private final String AUTH_TOKEN = "Bearer test-token"; - - @BeforeEach - void setUp() { - getUserDto = new GetUserDto(); - getUserDto.setKeycloakId(TEST_ID); - getUserDto.setUsername("testuser"); - - testUser = new UserEntity(); - testUser.setKeycloakId(TEST_ID); - testUser.setUsername("testuser"); - - createUserDto = new CreateUserDto(); - createUserDto.setKeycloakId(TEST_ID); - createUserDto.setUsername("testuser"); - } - - @Test - void getUserByIdSuccess() throws Exception { - when(userService.exists(TEST_ID)).thenReturn(true); - when(userService.getUser(TEST_ID)).thenReturn(getUserDto); - - mockMvc.perform(get("/user/" + TEST_ID)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.keycloakId").value(TEST_ID)) - .andExpect(jsonPath("$.username").value("testuser")); - } - - @Test - void getUserByIdNotFound() throws Exception { - when(userService.exists(TEST_ID)).thenReturn(false); - - mockMvc.perform(get("/user/" + TEST_ID)) - .andExpect(status().isNotFound()); - } - - @Test - void createUserSuccess() throws Exception { - when(userService.exists(TEST_ID)).thenReturn(false); - when(userService.createUser(any(CreateUserDto.class))).thenReturn(testUser); - - mockMvc.perform(post("/user") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createUserDto))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.keycloakId").value(TEST_ID)) - .andExpect(jsonPath("$.username").value("testuser")); - } - - @Test - void createUserAlreadyExists() throws Exception { - when(userService.exists(TEST_ID)).thenReturn(true); - - mockMvc.perform(post("/user") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createUserDto))) - .andExpect(status().isFound()) - .andExpect(header().string("Location", "/user/" + TEST_ID)); - } - - @Test - void getCurrentUserSuccess() throws Exception { - when(userService.getCurrentUser(AUTH_TOKEN)).thenReturn(getUserDto); - - mockMvc.perform(get("/user") - .header("Authorization", AUTH_TOKEN)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.keycloakId").value(TEST_ID)) - .andExpect(jsonPath("$.username").value("testuser")); - } - - @Test - void getCurrentUserNotFound() throws Exception { - when(userService.getCurrentUser(anyString())).thenReturn(null); - - mockMvc.perform(get("/user") - .header("Authorization", AUTH_TOKEN)) - .andExpect(status().isNotFound()); - } -} \ No newline at end of file