Merge pull request 'feat: Implement starting a Blackjack game (CAS-50)' (!89) from feat/blackjack into main
All checks were successful
Release / Release (push) Successful in 50s
All checks were successful
Release / Release (push) Successful in 50s
Reviewed-on: #89 Reviewed-by: Jan K9f <jan@kjan.email>
This commit is contained in:
commit
d0ba0eb71d
13 changed files with 283 additions and 125 deletions
7
backend/requests/blackjack.http
Normal file
7
backend/requests/blackjack.http
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
POST http://localhost:8080/blackjack/start
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"betAmount": 1.01
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
POST http://localhost:9090/realms/LF12/protocol/openid-connect/token
|
POST http://localhost:9090/realms/LF12/protocol/openid-connect/token
|
||||||
Content-Type: application/x-www-form-urlencoded
|
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); %}
|
> {% client.global.set("token", response.body.access_token); %}
|
|
@ -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<Object> createBlackJackGame(@RequestBody @Valid CreateBlackJackGameDto createBlackJackGameDto, @RequestHeader("Authorization") String token) {
|
||||||
|
Optional<UserEntity> 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<String, String> errorResponse = new HashMap<>();
|
||||||
|
errorResponse.put("error", "Invalid bet amount");
|
||||||
|
return ResponseEntity.badRequest().body(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (betAmount.compareTo(balance) > 0) {
|
||||||
|
Map<String, String> errorResponse = new HashMap<>();
|
||||||
|
errorResponse.put("error", "Insufficient funds");
|
||||||
|
return ResponseEntity.badRequest().body(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(blackJackService.createBlackJackGame(user, betAmount));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<CardEntity> playerCards = new ArrayList<>();
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
|
@JsonManagedReference
|
||||||
|
private List<CardEntity> dealerCards = new ArrayList<>();
|
||||||
|
}
|
|
@ -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<BlackJackGameEntity, Long> {
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
31
backend/src/main/java/de/szut/casino/blackjack/Rank.java
Normal file
31
backend/src/main/java/de/szut/casino/blackjack/Rank.java
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
20
backend/src/main/java/de/szut/casino/blackjack/Suit.java
Normal file
20
backend/src/main/java/de/szut/casino/blackjack/Suit.java
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -44,7 +44,7 @@ public class UserController {
|
||||||
|
|
||||||
@GetMapping("/user")
|
@GetMapping("/user")
|
||||||
public ResponseEntity<GetUserDto> getCurrentUser(@RequestHeader("Authorization") String token) {
|
public ResponseEntity<GetUserDto> getCurrentUser(@RequestHeader("Authorization") String token) {
|
||||||
GetUserDto userData = userService.getCurrentUser(token);
|
GetUserDto userData = userService.getCurrentUserAsDto(token);
|
||||||
|
|
||||||
if (userData == null) {
|
if (userData == null) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
|
|
|
@ -38,7 +38,7 @@ public class UserService {
|
||||||
return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null);
|
return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetUserDto getCurrentUser(String token) {
|
public GetUserDto getCurrentUserAsDto(String token) {
|
||||||
KeycloakUserDto userData = getKeycloakUserInfo(token);
|
KeycloakUserDto userData = getKeycloakUserInfo(token);
|
||||||
|
|
||||||
if (userData == null) {
|
if (userData == null) {
|
||||||
|
@ -47,7 +47,15 @@ public class UserService {
|
||||||
Optional<UserEntity> user = this.userRepository.findOneByKeycloakId(userData.getSub());
|
Optional<UserEntity> user = this.userRepository.findOneByKeycloakId(userData.getSub());
|
||||||
|
|
||||||
return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null);
|
return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<UserEntity> getCurrentUser(String token) {
|
||||||
|
KeycloakUserDto userData = getKeycloakUserInfo(token);
|
||||||
|
|
||||||
|
if (userData == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return this.userRepository.findOneByKeycloakId(userData.getSub());
|
||||||
}
|
}
|
||||||
|
|
||||||
private KeycloakUserDto getKeycloakUserInfo(String token) {
|
private KeycloakUserDto getKeycloakUserInfo(String token) {
|
||||||
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue