diff --git a/.env.example b/.env.example index 13be3183..a4a55986 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,7 @@ POSTGRES_HOST=localhost POSTGRES_PORT=5432 # backend -API_BASE_URL=http://localhost:8080 +API_BASE_URL=http://localhost:8080/ JWT_SECRET=SomeVeryLongRandomSecretKeyWhichIsUsedToEncodeJwtTokensForAuthorization # sentry diff --git a/.github/workflows/api-ci.yml b/.github/workflows/api-ci.yml index 88758cd6..4fd9730e 100644 --- a/.github/workflows/api-ci.yml +++ b/.github/workflows/api-ci.yml @@ -55,4 +55,5 @@ jobs: AWS_ENDPOINT: ${{ secrets.AWS_ENDPOINT }} AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }} AWS_SECRET_KEY: ${{ secrets.AWS_SECRET_KEY }} + JWT_SECRET: justforthisrunnerthishasnovaluelalalalalalalalalalalalalalalalalalalalalalalalalalla working-directory: api diff --git a/.github/workflows/generate-openapi.yaml b/.github/workflows/generate-openapi.yaml index 0787a80f..db0c5fd3 100644 --- a/.github/workflows/generate-openapi.yaml +++ b/.github/workflows/generate-openapi.yaml @@ -99,6 +99,7 @@ jobs: AWS_ENDPOINT: ${{ secrets.AWS_ENDPOINT }} AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }} AWS_SECRET_KEY: ${{ secrets.AWS_SECRET_KEY }} + JWT_SECRET: justforthisrunnerthishasnovaluelalalalalalalalalalalalalalalalalalalalalalalalalalla working-directory: api # replace all occurences of "*/*" with "application/json" in the openapi.json file @@ -130,3 +131,4 @@ jobs: if: steps.check_authors.outputs.has_bot_author != 'true' with: commit_message: "chore: update openapi types" + skip_checkout: true \ No newline at end of file diff --git a/api/pom.xml b/api/pom.xml index d7b74483..8a1eb413 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -18,6 +18,17 @@ 1.6.3 + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.6.0 + org.springframework.boot spring-boot-starter-data-jpa @@ -30,22 +41,9 @@ org.springframework.boot spring-boot-starter-websocket - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - - - - - io.sentry - sentry-spring-boot-starter-jakarta - 7.8.0 - - - io.sentry - sentry-logback - 7.8.0 + org.springframework.boot + spring-boot-starter-security @@ -65,11 +63,28 @@ test + + + io.sentry + sentry-spring-boot-starter-jakarta + 7.8.0 + + + io.sentry + sentry-logback + 7.8.0 + + + org.projectlombok lombok provided + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + org.mapstruct mapstruct @@ -80,27 +95,27 @@ lombok-mapstruct-binding 0.2.0 - software.amazon.awssdk s3 2.33.9 + - org.springframework.boot - spring-boot-starter-test - test + io.jsonwebtoken + jjwt-api + 0.11.5 - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.6.0 + io.jsonwebtoken + jjwt-impl + 0.11.5 - com.google.cloud.sql - postgres-socket-factory - 1.11.0 + io.jsonwebtoken + jjwt-jackson + 0.11.5 @@ -176,4 +191,5 @@ + diff --git a/api/src/main/java/pro/beerpong/api/auth/JwtAuthenticationFilter.java b/api/src/main/java/pro/beerpong/api/auth/JwtAuthenticationFilter.java new file mode 100644 index 00000000..3e44b701 --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/auth/JwtAuthenticationFilter.java @@ -0,0 +1,80 @@ +package pro.beerpong.api.auth; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import pro.beerpong.api.control.GroupController; +import pro.beerpong.api.service.AuthService; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.io.IOException; +import java.util.List; + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private static final String GROUPS_PATTERN = "/groups/**"; + private static final String GROUP_ID_PATTERN = "/groups/{groupId}/**"; + private static final List NO_VALIDATION_ENDPOINTS = List.of( + "/groups", + "/groups/" + GroupController.USER_GROUPS_ENDPOINT, + "/groups/{groupId}/" + GroupController.JOIN_GROUP_ENDPOINT + ); + + private final AuthService authService; + private final AntPathMatcher pathMatcher = new AntPathMatcher(); + + @Override + protected void doFilterInternal(HttpServletRequest req, + HttpServletResponse res, + FilterChain chain) throws ServletException, IOException { + var header = req.getHeader("Authorization"); + + if (!StringUtils.hasText(header) || !header.startsWith("Bearer ")) { + res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + res.getWriter().write("Missing or invalid Authorization header!"); + return; + } + + var token = header.substring(7); + var user = authService.userFromAccessToken(token); + + if (user == null) { + res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + res.getWriter().write("Invalid access token or invalid user-id!"); + return; + } + + if (!isExcludedFromValidation(req) && !authService.hasAccessToGroup(user, extractGroupId(req))) { + res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + res.getWriter().write("No access to this group!"); + return; + } + + var auth = new UsernamePasswordAuthenticationToken(user, null, List.of(new SimpleGrantedAuthority("ROLE_GROUP_MEMBER"))); + SecurityContextHolder.getContext().setAuthentication(auth); + + chain.doFilter(req, res); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + return !pathMatcher.match(GROUPS_PATTERN, request.getRequestURI()); + } + + private boolean isExcludedFromValidation(HttpServletRequest request) { + return NO_VALIDATION_ENDPOINTS.stream().anyMatch(s -> pathMatcher.match(s, request.getRequestURI())); + } + + private String extractGroupId(HttpServletRequest request) { + return (pathMatcher.match(GROUP_ID_PATTERN, request.getRequestURI()) ? + pathMatcher.extractUriTemplateVariables(GROUP_ID_PATTERN, request.getRequestURI()).get("groupId") : + ""); + } +} diff --git a/api/src/main/java/pro/beerpong/api/auth/JwtTokenProvider.java b/api/src/main/java/pro/beerpong/api/auth/JwtTokenProvider.java new file mode 100644 index 00000000..46de7ef3 --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/auth/JwtTokenProvider.java @@ -0,0 +1,66 @@ +package pro.beerpong.api.auth; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; + +@Component +public class JwtTokenProvider { + private Key key; + @Value("${jwt.secret}") + private String secret; + @Value("${jwt.access.expiration-ms}") + private long accessExpirationMs; + + @PostConstruct + public void init() { + key = Keys.hmacShaKeyFor(secret.getBytes()); + } + + public String createRefreshToken(String userId) { + return Jwts.builder() + .setSubject(userId) + .claim("type", "refresh") + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + public String createAccessToken(String userId) { + Date now = new Date(); + Date expiry = new Date(now.getTime() + accessExpirationMs); + + return Jwts.builder() + .setSubject(userId) + .claim("type", "access") + .setIssuedAt(now) + .setExpiration(expiry) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + public Jws parseToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token); + } + + public Claims validateToken(String token, String type) { + try { + var claims = parseToken(token).getBody(); + + if (!claims.get("type", String.class).equals(type)) { + return null; + } + + return claims; + } catch (JwtException | IllegalArgumentException e) { + return null; + } + } +} diff --git a/api/src/main/java/pro/beerpong/api/auth/SecurityConfig.java b/api/src/main/java/pro/beerpong/api/auth/SecurityConfig.java new file mode 100644 index 00000000..5022ecbf --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/auth/SecurityConfig.java @@ -0,0 +1,33 @@ +package pro.beerpong.api.auth; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import pro.beerpong.api.service.AuthService; + +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + private final AuthService authService; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + var jwtFilter = new JwtAuthenticationFilter(authService); + + http + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/groups/{groupId}/**").authenticated() + .anyRequest().permitAll() + ) + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} diff --git a/api/src/main/java/pro/beerpong/api/control/AuthController.java b/api/src/main/java/pro/beerpong/api/control/AuthController.java new file mode 100644 index 00000000..29bb0599 --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/control/AuthController.java @@ -0,0 +1,40 @@ +package pro.beerpong.api.control; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import pro.beerpong.api.model.dto.*; +import pro.beerpong.api.service.AuthService; + +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class AuthController { + private final AuthService authService; + + @PostMapping("signup") + public ResponseEntity> signup(@RequestBody AuthSignupDto authSignupDto) { + var result = authService.registerDevice(authSignupDto); + + if (result == null) { + return ResponseEnvelope.notOk(ErrorCodes.AUTH_REGISTER_INVALID_DTO); + } + + return ResponseEnvelope.ok(result); + } + + @PostMapping("refresh") + public ResponseEntity> refreshAuth(@RequestBody AuthRefreshDto dto) { + if (dto.getRefreshToken() == null || dto.getRefreshToken().trim().isEmpty()) { + return ResponseEnvelope.notOk(ErrorCodes.AUTH_REFRESH_INVALID_DTO); + } + + var result = authService.refreshAuth(dto); + + if (result == null) { + return ResponseEnvelope.notOk(ErrorCodes.AUTH_REFRESH_INVALID_TOKEN); + } + + return ResponseEnvelope.ok(result); + } +} diff --git a/api/src/main/java/pro/beerpong/api/control/GroupController.java b/api/src/main/java/pro/beerpong/api/control/GroupController.java index 19b0083e..8635582e 100644 --- a/api/src/main/java/pro/beerpong/api/control/GroupController.java +++ b/api/src/main/java/pro/beerpong/api/control/GroupController.java @@ -3,30 +3,46 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import pro.beerpong.api.model.dto.*; import pro.beerpong.api.service.AssetService; +import pro.beerpong.api.model.dto.*; +import pro.beerpong.api.service.AuthService; +import pro.beerpong.api.service.AssetService; import pro.beerpong.api.service.GroupService; import pro.beerpong.api.sockets.SocketEvent; import pro.beerpong.api.sockets.SocketEventData; import pro.beerpong.api.sockets.SubscriptionHandler; +import java.util.List; + @RestController @RequestMapping("/groups") public class GroupController { + public static final String USER_GROUPS_ENDPOINT = "user"; + public static final String JOIN_GROUP_ENDPOINT = "join"; + private final GroupService groupService; private final AssetService assetService; private final SubscriptionHandler subscriptionHandler; + private final AuthService authService; @Autowired - public GroupController(GroupService groupService, AssetService assetService, SubscriptionHandler subscriptionHandler) { + public GroupController(GroupService groupService, AssetService assetService, SubscriptionHandler subscriptionHandler, AuthService authService) { this.groupService = groupService; this.assetService = assetService; this.subscriptionHandler = subscriptionHandler; + this.authService = authService; } @PostMapping - public ResponseEntity> createGroup(@RequestBody GroupCreateDto groupCreateDto) { + public ResponseEntity> createGroup(@RequestBody GroupCreateDto groupCreateDto, + @AuthenticationPrincipal UserDto user) { + if (user == null) { + return ResponseEnvelope.notOk(ErrorCodes.AUTH_INVALID_USER); + } + if (groupCreateDto.invalidName()) { return ResponseEnvelope.notOk(ErrorCodes.INVALID_GROUP_NAME); } @@ -35,7 +51,7 @@ public ResponseEntity> createGroup(@RequestBody Group return ResponseEnvelope.notOk(ErrorCodes.INVALID_GROUP_PROFILE_NAMES); } - var group = groupService.createGroup(groupCreateDto); + var group = groupService.createGroup(groupCreateDto, user); if (group == null) { return ResponseEnvelope.notOk(ErrorCodes.INVALID_GROUP_SPORT); @@ -44,6 +60,17 @@ public ResponseEntity> createGroup(@RequestBody Group return ResponseEnvelope.ok(group); } + @GetMapping(USER_GROUPS_ENDPOINT) + public ResponseEntity>> findUserGroups(@AuthenticationPrincipal UserDto user) { + if (user == null) { + return ResponseEnvelope.notOk(ErrorCodes.AUTH_INVALID_USER); + } + + var groups = groupService.findGroupsByUser(user); + + return ResponseEnvelope.ok(groups); + } + @GetMapping public ResponseEntity> findGroupByInviteCode(@RequestParam String inviteCode) { if (inviteCode == null || inviteCode.trim().isEmpty()) { @@ -133,4 +160,40 @@ public ResponseEntity> deleteWallpaper(@PathVariable return ResponseEnvelope.ok(group); } + + @PostMapping("/{id}/" + JOIN_GROUP_ENDPOINT) + public ResponseEntity> joinGroup(@PathVariable String id, @AuthenticationPrincipal UserDto user) { + if (user == null) { + return ResponseEnvelope.notOk(ErrorCodes.AUTH_INVALID_USER); + } + + if (id == null || id.trim().isEmpty()) { + return ResponseEnvelope.notOk(ErrorCodes.INVALID_GROUP_ID); + } + + var groupMember = authService.joinGroup(user, id); + + if (groupMember != null) { + return ResponseEnvelope.ok("OK"); + } else { + return ResponseEnvelope.notOk(ErrorCodes.GROUP_ALREADY_IN_GROUP); + } + } + + @PostMapping("/{id}/leave") + public ResponseEntity> leaveGroup(@PathVariable String id, @AuthenticationPrincipal UserDto user) { + if (user == null) { + return ResponseEnvelope.notOk(ErrorCodes.AUTH_INVALID_USER); + } + + if (id == null || id.trim().isEmpty()) { + return ResponseEnvelope.notOk(ErrorCodes.INVALID_GROUP_ID); + } + + if (authService.leaveGroup(user, id)) { + return ResponseEnvelope.ok("OK"); + } else { + return ResponseEnvelope.notOk(ErrorCodes.AUTH_USER_NOT_IN_GROUP); + } + } } diff --git a/api/src/main/java/pro/beerpong/api/control/MatchController.java b/api/src/main/java/pro/beerpong/api/control/MatchController.java index 3ccb4246..f2257338 100644 --- a/api/src/main/java/pro/beerpong/api/control/MatchController.java +++ b/api/src/main/java/pro/beerpong/api/control/MatchController.java @@ -2,6 +2,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import pro.beerpong.api.model.dto.*; import pro.beerpong.api.service.MatchService; @@ -15,6 +16,10 @@ @RestController @RequestMapping("/groups/{groupId}/seasons/{seasonId}/matches") public class MatchController { + // currently we only support games played with exactly 2 teams + private static final int MIN_TEAM_AMOUNT = 2; + private static final int MAX_TEAM_AMOUNT = 2; + private final MatchService matchService; private final SeasonService seasonService; private final SubscriptionHandler subscriptionHandler; @@ -28,7 +33,12 @@ public MatchController(MatchService matchService, SeasonService seasonService, S @PostMapping public ResponseEntity> createMatch(@PathVariable String groupId, @PathVariable String seasonId, - @RequestBody MatchCreateDto matchCreateDto) { + @RequestBody MatchCreateDto matchCreateDto, + @AuthenticationPrincipal UserDto user) { + if (user == null) { + return ResponseEnvelope.notOk(ErrorCodes.AUTH_INVALID_USER); + } + var pair = seasonService.getSeasonAndGroup(groupId, seasonId); var error = seasonService.validateActiveSeason(MatchDto.class, pair); @@ -40,7 +50,11 @@ public ResponseEntity> createMatch(@PathVariable Stri return ResponseEnvelope.notOk(ErrorCodes.MATCH_CREATE_DTO_VALIDATION_FAILED); } - var match = matchService.createNewMatch(pair.getFirst(), pair.getSecond(), matchCreateDto); + if (matchCreateDto.getTeams().size() < MIN_TEAM_AMOUNT || matchCreateDto.getTeams().size() > MAX_TEAM_AMOUNT) { + return ResponseEnvelope.notOk(ErrorCodes.MATCH_WRONG_AMOUNT_OF_TEAMS); + } + + var match = matchService.createNewMatch(pair.getFirst(), pair.getSecond(), matchCreateDto, user); if (match != null) { if (match.getSeason().getId().equals(seasonId) && match.getSeason().getGroupId().equals(groupId)) { @@ -125,6 +139,10 @@ public ResponseEntity> updateMatch(@PathVariable Stri return ResponseEnvelope.notOk(ErrorCodes.MATCH_CREATE_DTO_VALIDATION_FAILED); } + if (matchCreateDto.getTeams().size() < MIN_TEAM_AMOUNT || matchCreateDto.getTeams().size() > MAX_TEAM_AMOUNT) { + return ResponseEnvelope.notOk(ErrorCodes.MATCH_WRONG_AMOUNT_OF_TEAMS); + } + if (matchCreateDto.getTeams().stream().anyMatch(teamCreateDto -> teamCreateDto.getExistingTeamId() == null)) { return ResponseEnvelope.notOk(ErrorCodes.MATCH_CREATE_DTO_NEEDS_IDS); } @@ -156,10 +174,7 @@ public ResponseEntity> deleteMatchById(@PathVariable St } @PutMapping("/{id}/photos/{teamId}") - public ResponseEntity> setPhoto(@PathVariable String groupId, - @PathVariable String seasonId, - @PathVariable String id, - @PathVariable String teamId) { + public ResponseEntity> setPhoto(@PathVariable String groupId, @PathVariable String teamId, @PathVariable String id, @PathVariable String seasonId) { var match = matchService.getMatchById(id); if (match == null) { @@ -186,10 +201,7 @@ public ResponseEntity> setPhoto(@PathVariable String g } @DeleteMapping("/{id}/photos/{teamId}") - public ResponseEntity> deletePhoto(@PathVariable String groupId, - @PathVariable String seasonId, - @PathVariable String id, - @PathVariable String teamId) { + public ResponseEntity> deletePhoto(@PathVariable String groupId, @PathVariable String teamId, @PathVariable String id, @PathVariable String seasonId) { var match = matchService.getMatchById(id); if (match == null) { diff --git a/api/src/main/java/pro/beerpong/api/control/PlayerController.java b/api/src/main/java/pro/beerpong/api/control/PlayerController.java index c805a1bc..d645b114 100644 --- a/api/src/main/java/pro/beerpong/api/control/PlayerController.java +++ b/api/src/main/java/pro/beerpong/api/control/PlayerController.java @@ -37,12 +37,22 @@ public ResponseEntity>> getPlayers(@PathVariabl return ResponseEnvelope.notOk(ErrorCodes.INVALID_SEASON_ID); } - var players = seasonService.calcStatsForPlayersInSeason(seasonId, showInactive, showStats); + var season = seasonService.getSeasonById(seasonId); + + if (season == null) { + return ResponseEnvelope.notOk(ErrorCodes.SEASON_NOT_FOUND); + } + + if (!season.getGroupId().equals(groupId)) { + return ResponseEnvelope.notOk(ErrorCodes.SEASON_NOT_OF_GROUP); + } + + var players = seasonService.calcStatsForPlayersInSeason(season, showInactive, showStats); if (players != null) { return ResponseEnvelope.ok(players); } else { - return ResponseEnvelope.notOk(ErrorCodes.SEASON_NOT_FOUND); + return ResponseEnvelope.notOk(ErrorCodes.ERROR); } } diff --git a/api/src/main/java/pro/beerpong/api/control/ProfileController.java b/api/src/main/java/pro/beerpong/api/control/ProfileController.java index 4310f62a..7f076abe 100644 --- a/api/src/main/java/pro/beerpong/api/control/ProfileController.java +++ b/api/src/main/java/pro/beerpong/api/control/ProfileController.java @@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import pro.beerpong.api.service.AssetService; import pro.beerpong.api.model.dto.*; @@ -25,11 +26,17 @@ public class ProfileController { private final SubscriptionHandler subscriptionHandler; @PostMapping - public ResponseEntity> createProfile(@PathVariable String groupId, @RequestBody ProfileCreateDto profileCreateDto) { + public ResponseEntity> createProfile(@PathVariable String groupId, + @RequestBody ProfileCreateDto profileCreateDto, + @AuthenticationPrincipal UserDto user) { + if (user == null) { + return ResponseEnvelope.notOk(ErrorCodes.AUTH_INVALID_USER); + } + var group = groupService.getGroupById(groupId); if (group != null) { - var dto = profileService.createPlayer(groupId, profileCreateDto); + var dto = profileService.createPlayer(groupId, profileCreateDto, user); if (dto == null) { return ResponseEnvelope.notOk(ErrorCodes.PROFILE_ALREADY_EXISTS); @@ -62,7 +69,11 @@ public ResponseEntity> getProfileById(@PathVariable var profile = profileService.getProfileById(id); if (profile != null) { - return ResponseEnvelope.ok(profile); + if (profile.getGroupId().equals(groupId)) { + return ResponseEnvelope.ok(profile); + } else { + return ResponseEnvelope.notOk(ErrorCodes.PROFILE_NOT_OF_GROUP); + } } else { return ResponseEnvelope.notOk(ErrorCodes.PROFILE_NOT_FOUND); } @@ -76,7 +87,7 @@ public ResponseEntity> updateProfile(@PathVariable var group = groupService.getGroupById(groupId); if (group != null) { - var updatedProfile = profileService.updateProfile(id, profileCreateDto); + var updatedProfile = profileService.updateProfile(id, groupId, profileCreateDto); if (updatedProfile != null) { subscriptionHandler.callEvent(new SocketEvent<>(SocketEventData.PROFILE_UPDATE, groupId, updatedProfile)); diff --git a/api/src/main/java/pro/beerpong/api/control/RuleController.java b/api/src/main/java/pro/beerpong/api/control/RuleController.java index a52615b2..77d30874 100644 --- a/api/src/main/java/pro/beerpong/api/control/RuleController.java +++ b/api/src/main/java/pro/beerpong/api/control/RuleController.java @@ -3,11 +3,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import pro.beerpong.api.model.dto.ErrorCodes; -import pro.beerpong.api.model.dto.ResponseEnvelope; -import pro.beerpong.api.model.dto.RuleCreateDto; -import pro.beerpong.api.model.dto.RuleDto; +import pro.beerpong.api.model.dto.*; import pro.beerpong.api.service.RuleService; import pro.beerpong.api.service.SeasonService; import pro.beerpong.api.sockets.SocketEvent; @@ -46,7 +44,14 @@ public ResponseEntity>> getRules(@PathVariable St } @PutMapping - public ResponseEntity>> writeRules(@PathVariable String groupId, @PathVariable String seasonId, @RequestBody List rules) { + public ResponseEntity>> writeRules(@PathVariable String groupId, + @PathVariable String seasonId, + @RequestBody List rules, + @AuthenticationPrincipal UserDto user) { + if (user == null) { + return ResponseEnvelope.notOk(ErrorCodes.AUTH_INVALID_USER); + } + var pair = seasonService.getSeasonAndGroup(groupId, seasonId); if (pair.getFirst() == null) { @@ -57,9 +62,11 @@ public ResponseEntity>> writeRules(@PathVariable return ResponseEnvelope.notOk(ErrorCodes.SEASON_NOT_OF_GROUP); } else if (pair.getSecond().getEndDate() != null) { return ResponseEnvelope.notOk(ErrorCodes.SEASON_ALREADY_ENDED); + } else if (rules.stream().anyMatch(RuleCreateDto::invalidDto)) { + return ResponseEnvelope.notOk(ErrorCodes.RULE_INVALID_DTO); } - var ruleDtos = ruleService.writeRules(groupId, pair.getSecond(), rules); + var ruleDtos = ruleService.writeRules(groupId, pair.getSecond(), rules, user); if (ruleDtos != null) { subscriptionHandler.callEvent(new SocketEvent<>(SocketEventData.RULES_WRITE, groupId, ruleDtos.toArray(new RuleDto[0]))); diff --git a/api/src/main/java/pro/beerpong/api/control/RuleMoveController.java b/api/src/main/java/pro/beerpong/api/control/RuleMoveController.java index 5da2d6af..3024a1ab 100644 --- a/api/src/main/java/pro/beerpong/api/control/RuleMoveController.java +++ b/api/src/main/java/pro/beerpong/api/control/RuleMoveController.java @@ -34,6 +34,10 @@ public ResponseEntity> createRuleMove(@PathVariabl return error; } + if (dto.invalidDto()) { + return ResponseEnvelope.notOk(ErrorCodes.RULE_MOVE_INVALID_DTO); + } + var move = moveService.createRuleMove(pair.getFirst(), pair.getSecond(), dto, true); if (move != null) { @@ -52,6 +56,10 @@ public ResponseEntity> updateRuleMove(@PathVariabl return error; } + if (dto.invalidDto()) { + return ResponseEnvelope.notOk(ErrorCodes.RULE_MOVE_INVALID_DTO); + } + var move = moveService.getById(ruleMoveId); if (move == null) { diff --git a/api/src/main/java/pro/beerpong/api/control/SeasonController.java b/api/src/main/java/pro/beerpong/api/control/SeasonController.java index 59033162..9195759b 100644 --- a/api/src/main/java/pro/beerpong/api/control/SeasonController.java +++ b/api/src/main/java/pro/beerpong/api/control/SeasonController.java @@ -1,12 +1,15 @@ package pro.beerpong.api.control; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import pro.beerpong.api.model.dto.*; import pro.beerpong.api.service.SeasonService; +import pro.beerpong.api.sockets.LocalTimeAdapter; +import java.time.LocalTime; +import java.time.format.DateTimeParseException; import java.util.List; @RestController @@ -20,7 +23,13 @@ public SeasonController(SeasonService seasonService) { } @PutMapping("/active-season") - public ResponseEntity> startNewSeason(@PathVariable String groupId, @RequestBody SeasonCreateDto dto) { + public ResponseEntity> startNewSeason(@PathVariable String groupId, + @RequestBody SeasonCreateDto dto, + @AuthenticationPrincipal UserDto user) { + if (user == null) { + return ResponseEnvelope.notOk(ErrorCodes.AUTH_INVALID_USER); + } + if (groupId == null || groupId.trim().isEmpty()) { return ResponseEnvelope.notOk(ErrorCodes.INVALID_GROUP_ID); } @@ -33,7 +42,7 @@ public ResponseEntity> startNewSeason(@PathVariable return ResponseEnvelope.notOk(ErrorCodes.INVALID_RULE_MOVES); } - var season = seasonService.startNewSeason(dto, groupId); + var season = seasonService.startNewSeason(dto, groupId, user); if (season != null) { return ResponseEnvelope.ok(season); @@ -94,15 +103,29 @@ public ResponseEntity> updateSeasonById(@PathVariabl return ResponseEnvelope.notOk(ErrorCodes.SEASON_ALREADY_ENDED); } - if (dto.getSeasonSettings().getWakeTimeHour() < 0 || dto.getSeasonSettings().getWakeTimeHour() > 23) { + var minMatches = (dto.getSeasonSettings().getMinMatchesToQualify() != null ? dto.getSeasonSettings().getMinMatchesToQualify() : season.get().getSeasonSettings().getMinMatchesToQualify()); + var minTeamSize = (dto.getSeasonSettings().getMinTeamSize() != null ? dto.getSeasonSettings().getMinTeamSize() : season.get().getSeasonSettings().getMinTeamSize()); + var maxTeamSize = (dto.getSeasonSettings().getMaxTeamSize() != null ? dto.getSeasonSettings().getMaxTeamSize() : season.get().getSeasonSettings().getMaxTeamSize()); + + LocalTime wakeTime = season.get().getSeasonSettings().getWakeTime(); + + if (dto.getSeasonSettings().getWakeTime() != null) { + try { + wakeTime = LocalTime.parse(dto.getSeasonSettings().getWakeTime(), LocalTimeAdapter.FORMATTER); + } catch (DateTimeParseException e) { + wakeTime = null; + } + } + + if (wakeTime == null) { return ResponseEnvelope.notOk(ErrorCodes.SEASON_WRONG_TIME_FORMAT); - } else if (dto.getSeasonSettings().getMinTeamSize() > dto.getSeasonSettings().getMaxTeamSize()) { + } else if (minTeamSize > maxTeamSize) { return ResponseEnvelope.notOk(ErrorCodes.SEASON_WRONG_TEAM_SIZES); } - dto.getSeasonSettings().setMinMatchesToQualify(Math.min(Math.max(dto.getSeasonSettings().getMinMatchesToQualify(), 0), 1000)); - dto.getSeasonSettings().setMinTeamSize(Math.min(Math.max(dto.getSeasonSettings().getMinTeamSize(), 1), 10)); - dto.getSeasonSettings().setMaxTeamSize(Math.min(Math.max(dto.getSeasonSettings().getMaxTeamSize(), 1), 10)); + dto.getSeasonSettings().setMinMatchesToQualify(Math.min(Math.max(minMatches, 0), 1000)); + dto.getSeasonSettings().setMinTeamSize(Math.min(Math.max(minTeamSize, 1), 10)); + dto.getSeasonSettings().setMaxTeamSize(Math.min(Math.max(maxTeamSize, 1), 10)); SeasonDto updatedSeason = seasonService.updateSeason(season.get(), dto); if (updatedSeason != null) { diff --git a/api/src/main/java/pro/beerpong/api/mapping/AssetMapper.java b/api/src/main/java/pro/beerpong/api/mapping/AssetMapper.java index 91b1053a..629dcaf2 100644 --- a/api/src/main/java/pro/beerpong/api/mapping/AssetMapper.java +++ b/api/src/main/java/pro/beerpong/api/mapping/AssetMapper.java @@ -4,7 +4,9 @@ import org.mapstruct.Mapping; import org.springframework.beans.factory.annotation.Value; import pro.beerpong.api.model.dao.Asset; +import pro.beerpong.api.model.dao.GroupMember; import pro.beerpong.api.model.dto.AssetMetadataDto; +import pro.beerpong.api.model.dto.GroupMemberDto; @Mapper(componentModel = "spring") public abstract class AssetMapper { @@ -16,7 +18,11 @@ public abstract class AssetMapper { @Mapping(target = "url", expression = "java(generateUrl(asset))") public abstract AssetMetadataDto assetToAssetMetadataDto(Asset asset); - public abstract Asset assetMetadataDtoToAsset(AssetMetadataDto asset); + public abstract Asset assetMetadataDtoToAsset(AssetMetadataDto dto); + + @Mapping(source = "group.id", target = "groupId") + @Mapping(source = "user.id", target = "userId") + public abstract GroupMemberDto groupMemberToGroupMemberDto(GroupMember groupMember); public String generateUrl(Asset asset) { return "https://" + bucket + "." + endpoint + "/" + asset.getId(); diff --git a/api/src/main/java/pro/beerpong/api/mapping/GroupMapper.java b/api/src/main/java/pro/beerpong/api/mapping/GroupMapper.java index f1bea6af..edda96ac 100644 --- a/api/src/main/java/pro/beerpong/api/mapping/GroupMapper.java +++ b/api/src/main/java/pro/beerpong/api/mapping/GroupMapper.java @@ -2,9 +2,7 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.springframework.web.util.UriComponentsBuilder; import pro.beerpong.api.control.GroupPresetsController; -import pro.beerpong.api.model.dao.Asset; import pro.beerpong.api.model.dao.Group; import pro.beerpong.api.model.dto.GroupCreateDto; import pro.beerpong.api.model.dto.GroupDto; diff --git a/api/src/main/java/pro/beerpong/api/mapping/GroupMemberMapper.java b/api/src/main/java/pro/beerpong/api/mapping/GroupMemberMapper.java new file mode 100644 index 00000000..2322fd92 --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/mapping/GroupMemberMapper.java @@ -0,0 +1,15 @@ +package pro.beerpong.api.mapping; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import pro.beerpong.api.model.dao.GroupMember; +import pro.beerpong.api.model.dao.Team; +import pro.beerpong.api.model.dto.GroupMemberDto; +import pro.beerpong.api.model.dto.TeamDto; + +@Mapper(componentModel = "spring") +public interface GroupMemberMapper { + @Mapping(source = "group.id", target = "groupId") + @Mapping(source = "user.id", target = "userId") + GroupMemberDto groupMemberToGroupMemberDto(GroupMember groupMember); +} diff --git a/api/src/main/java/pro/beerpong/api/mapping/RuleMapper.java b/api/src/main/java/pro/beerpong/api/mapping/RuleMapper.java index 4aaa0087..48f4cfa6 100644 --- a/api/src/main/java/pro/beerpong/api/mapping/RuleMapper.java +++ b/api/src/main/java/pro/beerpong/api/mapping/RuleMapper.java @@ -5,7 +5,7 @@ import pro.beerpong.api.model.dto.RuleCreateDto; import pro.beerpong.api.model.dto.RuleDto; -@Mapper(componentModel = "spring") +@Mapper(componentModel = "spring", uses = AssetMapper.class) public interface RuleMapper { Rule ruleCreateDtoToRule(RuleCreateDto dto); diff --git a/api/src/main/java/pro/beerpong/api/mapping/SeasonMapper.java b/api/src/main/java/pro/beerpong/api/mapping/SeasonMapper.java index c9dd7bb4..a909fb6e 100644 --- a/api/src/main/java/pro/beerpong/api/mapping/SeasonMapper.java +++ b/api/src/main/java/pro/beerpong/api/mapping/SeasonMapper.java @@ -4,7 +4,7 @@ import pro.beerpong.api.model.dao.Season; import pro.beerpong.api.model.dto.SeasonDto; -@Mapper(componentModel = "spring") +@Mapper(componentModel = "spring", uses = AssetMapper.class) public interface SeasonMapper { SeasonDto seasonToSeasonDto(Season season); } \ No newline at end of file diff --git a/api/src/main/java/pro/beerpong/api/mapping/SeasonSettingsMapper.java b/api/src/main/java/pro/beerpong/api/mapping/SeasonSettingsMapper.java new file mode 100644 index 00000000..6abe0388 --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/mapping/SeasonSettingsMapper.java @@ -0,0 +1,19 @@ +package pro.beerpong.api.mapping; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import pro.beerpong.api.model.dao.SeasonSettings; +import pro.beerpong.api.model.dto.SeasonSettingsDto; +import pro.beerpong.api.sockets.LocalTimeAdapter; + +import java.time.LocalTime; + +@Mapper(componentModel = "spring") +public abstract class SeasonSettingsMapper { + @Mapping(target = "wakeTime", expression = "java(parseTime(seasonSettingsDto))") + public abstract SeasonSettings seasonSettingsDtoToSeasonSettings(SeasonSettingsDto seasonSettingsDto); + + protected LocalTime parseTime(SeasonSettingsDto seasonSettingsDto) { + return LocalTime.parse(seasonSettingsDto.getWakeTime(), LocalTimeAdapter.FORMATTER); + } +} \ No newline at end of file diff --git a/api/src/main/java/pro/beerpong/api/mapping/UserMapper.java b/api/src/main/java/pro/beerpong/api/mapping/UserMapper.java new file mode 100644 index 00000000..a6830bf0 --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/mapping/UserMapper.java @@ -0,0 +1,15 @@ +package pro.beerpong.api.mapping; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import pro.beerpong.api.model.dao.Team; +import pro.beerpong.api.model.dao.User; +import pro.beerpong.api.model.dto.TeamDto; +import pro.beerpong.api.model.dto.UserDto; + +@Mapper(componentModel = "spring") +public interface UserMapper { + UserDto userToUserDto(User user); + + User userDtoToUser(UserDto userDto); +} diff --git a/api/src/main/java/pro/beerpong/api/model/dao/Device.java b/api/src/main/java/pro/beerpong/api/model/dao/Device.java new file mode 100644 index 00000000..c8ba5cff --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/model/dao/Device.java @@ -0,0 +1,19 @@ +package pro.beerpong.api.model.dao; + +import jakarta.persistence.*; +import lombok.Data; +import pro.beerpong.api.util.InstallationType; + +@Entity(name = "devices") +@Data +public class Device { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + private InstallationType type; + private String deviceId; + private String pushNotificationToken; + @ManyToOne + @JoinColumn(name = "user_id") + private User user; +} diff --git a/api/src/main/java/pro/beerpong/api/model/dao/Group.java b/api/src/main/java/pro/beerpong/api/model/dao/Group.java index 676fe545..057c49ce 100644 --- a/api/src/main/java/pro/beerpong/api/model/dao/Group.java +++ b/api/src/main/java/pro/beerpong/api/model/dao/Group.java @@ -5,6 +5,7 @@ import pro.beerpong.api.model.dto.GroupPreset; import java.time.ZonedDateTime; +import java.util.List; @Entity(name = "groups") @Data @@ -21,6 +22,11 @@ public class Group { @OneToOne @JoinColumn(name = "assetIdWallpaper") private Asset wallpaperAsset; + @OneToMany(mappedBy = "group") + private List members; + @ManyToOne + @JoinColumn(name = "createdBy") + private GroupMember createdBy; private ZonedDateTime createdAt; private String sportPreset; private String customSportName; diff --git a/api/src/main/java/pro/beerpong/api/model/dao/GroupMember.java b/api/src/main/java/pro/beerpong/api/model/dao/GroupMember.java new file mode 100644 index 00000000..741678c5 --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/model/dao/GroupMember.java @@ -0,0 +1,21 @@ +package pro.beerpong.api.model.dao; + +import jakarta.persistence.*; +import lombok.Data; + +import java.util.List; + +@Entity(name = "group_members") +@Data +public class GroupMember { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + private boolean active; + @ManyToOne + @JoinColumn(name = "group_id") + private Group group; + @ManyToOne + @JoinColumn(name = "user_id") + private User user; +} diff --git a/api/src/main/java/pro/beerpong/api/model/dao/Match.java b/api/src/main/java/pro/beerpong/api/model/dao/Match.java index 8a3cd76a..3bcd3ebc 100644 --- a/api/src/main/java/pro/beerpong/api/model/dao/Match.java +++ b/api/src/main/java/pro/beerpong/api/model/dao/Match.java @@ -21,4 +21,8 @@ public class Match { @OneToMany(mappedBy = "match") private List teams; + + @ManyToOne + @JoinColumn(name = "createdBy") + private GroupMember createdBy; } diff --git a/api/src/main/java/pro/beerpong/api/model/dao/Profile.java b/api/src/main/java/pro/beerpong/api/model/dao/Profile.java index 9cd80327..fa79a51f 100644 --- a/api/src/main/java/pro/beerpong/api/model/dao/Profile.java +++ b/api/src/main/java/pro/beerpong/api/model/dao/Profile.java @@ -18,4 +18,8 @@ public class Profile { @ManyToOne @JoinColumn(name = "groupId") private Group group; + + @ManyToOne + @JoinColumn(name = "createdBy") + private GroupMember createdBy; } diff --git a/api/src/main/java/pro/beerpong/api/model/dao/Rule.java b/api/src/main/java/pro/beerpong/api/model/dao/Rule.java index c9c0924e..ed6df542 100644 --- a/api/src/main/java/pro/beerpong/api/model/dao/Rule.java +++ b/api/src/main/java/pro/beerpong/api/model/dao/Rule.java @@ -6,7 +6,7 @@ @Entity(name = "rules") @Data -public class Rule implements Cloneable { +public class Rule { @Id @GeneratedValue(strategy = GenerationType.UUID) private String id; @@ -20,9 +20,7 @@ public class Rule implements Cloneable { @JoinColumn(name = "seasonId") private Season season; - @Override - @SneakyThrows - public Rule clone() { - return (Rule) super.clone(); - } + @ManyToOne + @JoinColumn(name = "createdBy") + private GroupMember createdBy; } diff --git a/api/src/main/java/pro/beerpong/api/model/dao/RuleMove.java b/api/src/main/java/pro/beerpong/api/model/dao/RuleMove.java index 933dc6a6..b3b5ce92 100644 --- a/api/src/main/java/pro/beerpong/api/model/dao/RuleMove.java +++ b/api/src/main/java/pro/beerpong/api/model/dao/RuleMove.java @@ -8,7 +8,7 @@ @Entity(name = "rule_moves") @Data -public class RuleMove implements Cloneable { +public class RuleMove { @Id @GeneratedValue(strategy = GenerationType.UUID) private String id; @@ -27,10 +27,4 @@ public class RuleMove implements Cloneable { @OneToMany(mappedBy = "move") private List matchMoves; - - @Override - @SneakyThrows - public RuleMove clone() { - return (RuleMove) super.clone(); - } } \ No newline at end of file diff --git a/api/src/main/java/pro/beerpong/api/model/dao/Season.java b/api/src/main/java/pro/beerpong/api/model/dao/Season.java index 4d111729..bbbdd48f 100644 --- a/api/src/main/java/pro/beerpong/api/model/dao/Season.java +++ b/api/src/main/java/pro/beerpong/api/model/dao/Season.java @@ -23,4 +23,8 @@ public class Season { @OneToOne(cascade = CascadeType.ALL) @JoinColumn private SeasonSettings seasonSettings; + + @ManyToOne + @JoinColumn(name = "createdBy") + private GroupMember createdBy; } diff --git a/api/src/main/java/pro/beerpong/api/model/dao/SeasonSettings.java b/api/src/main/java/pro/beerpong/api/model/dao/SeasonSettings.java index 7752c3fe..a4a4efa2 100644 --- a/api/src/main/java/pro/beerpong/api/model/dao/SeasonSettings.java +++ b/api/src/main/java/pro/beerpong/api/model/dao/SeasonSettings.java @@ -1,13 +1,12 @@ package pro.beerpong.api.model.dao; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import jakarta.persistence.*; import lombok.Data; import pro.beerpong.api.util.DailyLeaderboard; import pro.beerpong.api.util.RankingAlgorithm; +import java.time.LocalTime; + @Entity(name = "season_settings") @Data public class SeasonSettings { @@ -15,10 +14,24 @@ public class SeasonSettings { @GeneratedValue(strategy = GenerationType.UUID) private String id; - private int minMatchesToQualify = 1; - private int minTeamSize = 1; - private int maxTeamSize = 10; - private RankingAlgorithm rankingAlgorithm = RankingAlgorithm.AVERAGE; - private DailyLeaderboard dailyLeaderboard = DailyLeaderboard.WAKE_TIME; - private int wakeTimeHour = 0; + private int minMatchesToQualify; + private int minTeamSize; + private int maxTeamSize; + private RankingAlgorithm rankingAlgorithm; + private DailyLeaderboard dailyLeaderboard; + @Column(columnDefinition = "time default '00:00:00'") + private LocalTime wakeTime; + + public static SeasonSettings createDefault() { + var seasonSettings = new SeasonSettings(); + + seasonSettings.setMinMatchesToQualify(1); + seasonSettings.setMinTeamSize(1); + seasonSettings.setMaxTeamSize(10); + seasonSettings.setRankingAlgorithm(RankingAlgorithm.AVERAGE); + seasonSettings.setDailyLeaderboard(DailyLeaderboard.WAKE_TIME); + seasonSettings.setWakeTime(LocalTime.of(0, 0)); + + return seasonSettings; + } } diff --git a/api/src/main/java/pro/beerpong/api/model/dao/User.java b/api/src/main/java/pro/beerpong/api/model/dao/User.java new file mode 100644 index 00000000..0b93c66e --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/model/dao/User.java @@ -0,0 +1,22 @@ +package pro.beerpong.api.model.dao; + +import jakarta.persistence.*; +import lombok.Data; + +import java.util.List; + +@Entity(name = "users") +@Data +public class User { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + + @OneToMany(mappedBy = "user") + private List devices; + + @OneToMany(mappedBy = "user") + private List groups; + + // future: premium subscriptions and other stuff here +} diff --git a/api/src/main/java/pro/beerpong/api/model/dto/AuthRefreshDto.java b/api/src/main/java/pro/beerpong/api/model/dto/AuthRefreshDto.java new file mode 100644 index 00000000..4b6c7446 --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/model/dto/AuthRefreshDto.java @@ -0,0 +1,9 @@ +package pro.beerpong.api.model.dto; + +import lombok.Data; +import pro.beerpong.api.util.InstallationType; + +@Data +public class AuthRefreshDto { + private String refreshToken; +} \ No newline at end of file diff --git a/api/src/main/java/pro/beerpong/api/model/dto/AuthSignupDto.java b/api/src/main/java/pro/beerpong/api/model/dto/AuthSignupDto.java new file mode 100644 index 00000000..1c22ede4 --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/model/dto/AuthSignupDto.java @@ -0,0 +1,10 @@ +package pro.beerpong.api.model.dto; + +import lombok.Data; +import pro.beerpong.api.util.InstallationType; + +@Data +public class AuthSignupDto { + private InstallationType installationType; + private String deviceId; +} \ No newline at end of file diff --git a/api/src/main/java/pro/beerpong/api/model/dto/AuthTokenDto.java b/api/src/main/java/pro/beerpong/api/model/dto/AuthTokenDto.java new file mode 100644 index 00000000..a3b5ee4e --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/model/dto/AuthTokenDto.java @@ -0,0 +1,12 @@ +package pro.beerpong.api.model.dto; + +import lombok.Data; +import pro.beerpong.api.util.TokenType; + +import java.time.ZonedDateTime; + +@Data +public class AuthTokenDto { + private String token; + private TokenType type; +} \ No newline at end of file diff --git a/api/src/main/java/pro/beerpong/api/model/dto/ErrorCodes.java b/api/src/main/java/pro/beerpong/api/model/dto/ErrorCodes.java index 7d0717a4..8dfe57b0 100644 --- a/api/src/main/java/pro/beerpong/api/model/dto/ErrorCodes.java +++ b/api/src/main/java/pro/beerpong/api/model/dto/ErrorCodes.java @@ -8,10 +8,12 @@ @Getter @RequiredArgsConstructor public enum ErrorCodes { - /* GROUPS */ + ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "error", "An internal error occurred!") + /* GROUPS */, GROUP_NOT_FOUND(HttpStatus.NOT_FOUND, "groupNotFound", "The requested group could not be found!"), GROUP_INVITE_NOT_FOUND(HttpStatus.NOT_FOUND, "groupInviteNotFound", "No group with the provided invite code could be found!"), GROUP_INVITE_CODE_NOT_PROVIDED(HttpStatus.BAD_REQUEST, "groupInviteCodeNotProvided", "The invite code needs to be provided!"), + GROUP_ALREADY_IN_GROUP(HttpStatus.FORBIDDEN, "groupAlreadyInGroup", "The user is already a member of this group!"), GROUP_HAS_NO_WALLPAPER(HttpStatus.NOT_FOUND, "groupHasNoWallpaper", "The provided group does not have a wallpaper saved!"), INVALID_GROUP_NAME(HttpStatus.BAD_REQUEST, "invalidGroupName", "Group name must be non-null, non-empty and between 2 and 50 characters!"), INVALID_GROUP_PROFILE_NAMES(HttpStatus.BAD_REQUEST, "invalidGroupProfileNames", "Group profileNames must be non-null and non-empty!"), @@ -21,7 +23,7 @@ public enum ErrorCodes { /* SEASONS */, SEASON_NOT_FOUND(HttpStatus.NOT_FOUND, "seasonNotFound", "The requested season could not be found!"), SEASON_ALREADY_ENDED(HttpStatus.FORBIDDEN, "seasonAlreadyEnded", "Past seasons are immutable!"), - SEASON_WRONG_TIME_FORMAT(HttpStatus.BAD_REQUEST, "seasonWrongTimeFormat", "The wake time hour has to be between 0 and 23!"), + SEASON_WRONG_TIME_FORMAT(HttpStatus.BAD_REQUEST, "seasonWrongTimeFormat", "The wake time has to be supplied in the following format: HH:mm and be a valid hour and minute"), SEASON_WRONG_TEAM_SIZES(HttpStatus.BAD_REQUEST, "seasonWrongTeamSizes", "The min team size has to be less then or equal to the max team size!"), SEASON_NOT_OF_GROUP(HttpStatus.FORBIDDEN, "seasonHasDifferentGroup", "The seasons group id does not match the provided group id!"), SEASON_VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "seasonValidationFailed", "The validation of the created season has failed (invalid group id)"), @@ -33,31 +35,41 @@ public enum ErrorCodes { MATCH_NOT_FOUND(HttpStatus.NOT_FOUND, "matchNotFound", "The requested match could not be found!"), MATCH_VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "matchValidationFailed", "The validation of the created match has failed (invalid group or season id)"), MATCH_CREATE_DTO_VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "matchCreateDtoValidationFailed", "The team sizes are not in the boundaries of the season settings!"), + MATCH_WRONG_AMOUNT_OF_TEAMS(HttpStatus.BAD_REQUEST, "matchWrongAmountOfTeams", "The amount of teams has to be exactly 2!"), + MATCH_DTO_VALIDATION_FAILED(HttpStatus.FORBIDDEN, "matchDtoValidationFailed", "The validation of the match create dto failed (invalid player, rulemove, season or group id, double player, or no/more than exactly one finish move)"), MATCH_CREATE_DTO_NEEDS_IDS(HttpStatus.BAD_REQUEST, "matchCreateDtoNeedsIds", "Every team needs a 'existingTeamId' attribute containing the id of the existing team!"), - MATCH_DTO_VALIDATION_FAILED(HttpStatus.FORBIDDEN, "matchDtoValidationFailed", "The validation of the match create dto failed (invalid player, rulemove, season or group id, or no finish move)"), MATCH_NO_TEAM_FOUND(HttpStatus.FORBIDDEN, "matchNoTeamFound", "Could not find a team with the provided id linked to the provided match!"), MATCH_TEAM_HAS_NO_PHOTO(HttpStatus.NOT_FOUND, "matchTeamHasNoPhoto", "The provided team does not have a photo set!"), MATCH_GROUP_OR_SEASON_ID_DONT_MATCH(HttpStatus.BAD_REQUEST, "matchGroupOrSeasonIdDontMatch", "The provided group and season id dont match the group and season id of the provided match!") /* RULE MOVES */, RULE_MOVE_NOT_FOUND(HttpStatus.NOT_FOUND, "ruleMoveNotFound", "The requested ruleMove could not be found!"), RULE_MOVE_VALIDATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ruleMoveValidationFailed", "The rulemove is not part of the provided season or the provided season is not part of the provided gorup!"), - RULE_VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "ruleValidationFailed", "The validation of the created rules has failed (invalid group or season id)") - /* PLAYERS */, + RULE_MOVE_INVALID_DTO(HttpStatus.BAD_REQUEST, "ruleMoveInvalidDto", "The name has to be non-null and non-empty and pointsForScorer and pointsForTeam have to be >= 0!"), + RULE_VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "ruleValidationFailed", "The validation of the created rules has failed (invalid group or season id)"), + /* RULES */ + RULE_INVALID_DTO(HttpStatus.BAD_REQUEST, "ruleInvalidDto", "Every rule name and description has to be non-null and non-empty!"), + /* PLAYERS */ PLAYER_VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "playerValidationFailed", "The player is not part of the provided season or group!"), PLAYER_NOT_FOUND(HttpStatus.NOT_FOUND, "playerNotFound", "The requested player could not be found!"), PLAYER_ALREADY_DELETED(HttpStatus.FORBIDDEN, "playerAlreadyDeleted", "This player has been deleted!"), INVALID_PLAYER_ID(HttpStatus.BAD_REQUEST, "invalidPlayerId", "Player id must be non-null and non-empty!") /* PROFILES */, PROFILE_NOT_FOUND(HttpStatus.NOT_FOUND, "profileNotFound", "The requested profile could not be found!"), + PROFILE_NOT_OF_GROUP(HttpStatus.BAD_REQUEST, "profileNotOfGroup", "The provided profile and group id do not match!"), PROFILE_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "profileAlreadyExists", "There already exists a profile with the provided name and an active player in the current season!"), - PROFILE_NOT_OF_GROUP(HttpStatus.FORBIDDEN, "profileHasDifferentGroup", "The profiles group id does not match the provided group id!"), PROFILE_HAS_NO_AVATAR(HttpStatus.NOT_FOUND, "profileHasNoAvatar", "The provided profiles does not have an avatar saved!") /* ASSETS */, ASSET_NOT_FOUND(HttpStatus.NOT_FOUND, "assetNotFound", "The requested asset could not be found!"), ASSET_VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "assetValidationFailed", "The provided asset offsets or zoom have to be >= 0!"), /* LEADERBOARDS */ LEADERBOARD_SCOPE_NOT_FOUND(HttpStatus.NOT_FOUND, "leaderboardScopeNotFound", "The leaderboard scope has to be one of: all-time, today, season"), - LEADERBOARD_SEASON_NOT_FOUND(HttpStatus.NOT_FOUND, "leaderboardScopeNotFound", "The scope 'season' requires a seasonId param!"); + LEADERBOARD_SEASON_NOT_FOUND(HttpStatus.NOT_FOUND, "leaderboardScopeNotFound", "The scope 'season' requires a seasonId param!"), + /* AUTH */ + AUTH_REGISTER_INVALID_DTO(HttpStatus.BAD_REQUEST, "authRegisterInvalidDto", "The installationType or deviceId is invalid!"), + AUTH_REFRESH_INVALID_DTO(HttpStatus.BAD_REQUEST, "authRefreshInvalidDto", "The refreshToken has to be non-null and non-empty!"), + AUTH_INVALID_USER(HttpStatus.UNAUTHORIZED, "authInvalidUser", "The user from the access token has to be non-null!"), + AUTH_USER_NOT_IN_GROUP(HttpStatus.UNAUTHORIZED, "authUserNotInGroup", "The user is not in this group!"), + AUTH_REFRESH_INVALID_TOKEN(HttpStatus.BAD_REQUEST, "authRefreshInvalidToken", "The refreshToken is no refresh-token, invalid or the subject-userId is invalid!"); private final HttpStatus httpStatus; private final String code; diff --git a/api/src/main/java/pro/beerpong/api/model/dto/GroupCreateDto.java b/api/src/main/java/pro/beerpong/api/model/dto/GroupCreateDto.java index 6565da8c..4b4bbb06 100644 --- a/api/src/main/java/pro/beerpong/api/model/dto/GroupCreateDto.java +++ b/api/src/main/java/pro/beerpong/api/model/dto/GroupCreateDto.java @@ -16,11 +16,12 @@ public class GroupCreateDto { private String customSportName; public boolean invalidName() { - return this.name == null || this.name.isEmpty() || + return this.name == null || this.name.trim().isEmpty() || this.name.length() < GROUP_NAME_MIN_LENGTH || this.name.length() > GROUP_NAME_MAX_LENGTH; } public boolean invalidProfileName() { - return this.profileNames == null || this.profileNames.isEmpty(); + return this.profileNames == null || this.profileNames.isEmpty() || + this.profileNames.stream().anyMatch(s -> this.profileNames.stream().filter(s1 -> s1.equals(s)).count() > 1); } } diff --git a/api/src/main/java/pro/beerpong/api/model/dto/GroupDto.java b/api/src/main/java/pro/beerpong/api/model/dto/GroupDto.java index a1560a87..796b33a0 100644 --- a/api/src/main/java/pro/beerpong/api/model/dto/GroupDto.java +++ b/api/src/main/java/pro/beerpong/api/model/dto/GroupDto.java @@ -11,9 +11,10 @@ public class GroupDto { private String id; private String name; private String inviteCode; - private Season activeSeason; + private SeasonDto activeSeason; @JsonInclude(JsonInclude.Include.NON_NULL) private AssetMetadataDto wallpaperAsset; + private GroupMemberDto createdBy; private ZonedDateTime createdAt; private GroupPreset sportPreset; private String customSportName; diff --git a/api/src/main/java/pro/beerpong/api/model/dto/GroupMemberDto.java b/api/src/main/java/pro/beerpong/api/model/dto/GroupMemberDto.java new file mode 100644 index 00000000..fca15a01 --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/model/dto/GroupMemberDto.java @@ -0,0 +1,11 @@ +package pro.beerpong.api.model.dto; + +import lombok.Data; + +@Data +public class GroupMemberDto { + private String id; + private boolean active; + private String groupId; + private String userId; +} diff --git a/api/src/main/java/pro/beerpong/api/model/dto/MatchDto.java b/api/src/main/java/pro/beerpong/api/model/dto/MatchDto.java index e050ba36..d376ab66 100644 --- a/api/src/main/java/pro/beerpong/api/model/dto/MatchDto.java +++ b/api/src/main/java/pro/beerpong/api/model/dto/MatchDto.java @@ -1,7 +1,6 @@ package pro.beerpong.api.model.dto; import lombok.Data; -import pro.beerpong.api.model.dao.Season; import java.time.ZonedDateTime; import java.util.List; @@ -10,7 +9,8 @@ public class MatchDto { private String id; private ZonedDateTime date; - private Season season; + private SeasonDto season; + private GroupMemberDto createdBy; private List teams; private List teamMembers; private List matchMoves; diff --git a/api/src/main/java/pro/beerpong/api/model/dto/MatchOverviewDto.java b/api/src/main/java/pro/beerpong/api/model/dto/MatchOverviewDto.java index f047bee3..3e471c71 100644 --- a/api/src/main/java/pro/beerpong/api/model/dto/MatchOverviewDto.java +++ b/api/src/main/java/pro/beerpong/api/model/dto/MatchOverviewDto.java @@ -10,7 +10,7 @@ public class MatchOverviewDto { private String id; private ZonedDateTime date; - private Season season; + private SeasonDto season; private MatchOverviewTeamDto blueTeam; private MatchOverviewTeamDto redTeam; diff --git a/api/src/main/java/pro/beerpong/api/model/dto/ProfileCreatedDto.java b/api/src/main/java/pro/beerpong/api/model/dto/ProfileCreatedDto.java index 4c91b0b4..86792ef3 100644 --- a/api/src/main/java/pro/beerpong/api/model/dto/ProfileCreatedDto.java +++ b/api/src/main/java/pro/beerpong/api/model/dto/ProfileCreatedDto.java @@ -1,23 +1,29 @@ package pro.beerpong.api.model.dto; -import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import org.springframework.lang.Nullable; @EqualsAndHashCode(callSuper = true) @Data +@NoArgsConstructor public class ProfileCreatedDto extends ProfileDto { private boolean reactivated; @Nullable private String lastActiveSeasonId; - public ProfileCreatedDto(ProfileDto profile, boolean reactivated, @Nullable String lastActiveSeasonId) { - setId(profile.getId()); - setName(profile.getName()); - setAvatarAsset(profile.getAvatarAsset()); - setGroupId(profile.getGroupId()); + public ProfileCreatedDto(String id, String name, AssetMetadataDto asset, String groupId, GroupMemberDto createdBy, boolean reactivated, @Nullable String lastActiveSeasonId) { + setId(id); + setName(name); + setAvatarAsset(asset); + setGroupId(groupId); + setCreatedBy(createdBy); this.reactivated = reactivated; this.lastActiveSeasonId = lastActiveSeasonId; } + + public ProfileCreatedDto(ProfileDto profile, boolean reactivated, @Nullable String lastActiveSeasonId) { + this(profile.getId(), profile.getName(), profile.getAvatarAsset(), profile.getGroupId(), profile.getCreatedBy(), reactivated, lastActiveSeasonId); + } } diff --git a/api/src/main/java/pro/beerpong/api/model/dto/ProfileDto.java b/api/src/main/java/pro/beerpong/api/model/dto/ProfileDto.java index 9b276635..ee6df307 100644 --- a/api/src/main/java/pro/beerpong/api/model/dto/ProfileDto.java +++ b/api/src/main/java/pro/beerpong/api/model/dto/ProfileDto.java @@ -10,4 +10,5 @@ public class ProfileDto { @JsonInclude(JsonInclude.Include.NON_NULL) private AssetMetadataDto avatarAsset; private String groupId; + private GroupMemberDto createdBy; } diff --git a/api/src/main/java/pro/beerpong/api/model/dto/RuleCreateDto.java b/api/src/main/java/pro/beerpong/api/model/dto/RuleCreateDto.java index 2448dea7..c9cee991 100644 --- a/api/src/main/java/pro/beerpong/api/model/dto/RuleCreateDto.java +++ b/api/src/main/java/pro/beerpong/api/model/dto/RuleCreateDto.java @@ -6,4 +6,9 @@ public class RuleCreateDto { private String title; private String description; + + public boolean invalidDto() { + return this.title == null || this.title.trim().isEmpty() || + this.description == null || this.description.trim().isEmpty(); + } } \ No newline at end of file diff --git a/api/src/main/java/pro/beerpong/api/model/dto/RuleDto.java b/api/src/main/java/pro/beerpong/api/model/dto/RuleDto.java index 1907d02d..9c0657bf 100644 --- a/api/src/main/java/pro/beerpong/api/model/dto/RuleDto.java +++ b/api/src/main/java/pro/beerpong/api/model/dto/RuleDto.java @@ -1,12 +1,12 @@ package pro.beerpong.api.model.dto; import lombok.Data; -import pro.beerpong.api.model.dao.Season; @Data public class RuleDto { private String id; private String title; private String description; - private Season season; + private SeasonDto season; + private GroupMemberDto createdBy; } \ No newline at end of file diff --git a/api/src/main/java/pro/beerpong/api/model/dto/RuleMoveCreateDto.java b/api/src/main/java/pro/beerpong/api/model/dto/RuleMoveCreateDto.java index 524b288a..4bbc3936 100644 --- a/api/src/main/java/pro/beerpong/api/model/dto/RuleMoveCreateDto.java +++ b/api/src/main/java/pro/beerpong/api/model/dto/RuleMoveCreateDto.java @@ -10,7 +10,7 @@ public class RuleMoveCreateDto { private boolean finishingMove; public boolean invalidDto() { - return this.name == null || this.name.isEmpty() || + return this.name == null || this.name.trim().isEmpty() || this.pointsForTeam < 0 || this.pointsForScorer < 0; } } \ No newline at end of file diff --git a/api/src/main/java/pro/beerpong/api/model/dto/RuleMoveDto.java b/api/src/main/java/pro/beerpong/api/model/dto/RuleMoveDto.java index 5978e754..e409253a 100644 --- a/api/src/main/java/pro/beerpong/api/model/dto/RuleMoveDto.java +++ b/api/src/main/java/pro/beerpong/api/model/dto/RuleMoveDto.java @@ -10,5 +10,5 @@ public class RuleMoveDto { private int pointsForTeam; private int pointsForScorer; private boolean finishingMove; - private Season season; + private SeasonDto season; } \ No newline at end of file diff --git a/api/src/main/java/pro/beerpong/api/model/dto/SeasonCreateDto.java b/api/src/main/java/pro/beerpong/api/model/dto/SeasonCreateDto.java index 94bda73f..08770cdd 100644 --- a/api/src/main/java/pro/beerpong/api/model/dto/SeasonCreateDto.java +++ b/api/src/main/java/pro/beerpong/api/model/dto/SeasonCreateDto.java @@ -13,7 +13,7 @@ public class SeasonCreateDto { private List ruleMoves; public boolean invalidName() { - return this.oldSeasonName == null || this.oldSeasonName.isEmpty() || + return this.oldSeasonName == null || this.oldSeasonName.trim().isEmpty() || this.oldSeasonName.length() < SEASON_NAME_MIN_LENGTH || this.oldSeasonName.length() > SEASON_NAME_MAX_LENGTH; } diff --git a/api/src/main/java/pro/beerpong/api/model/dto/SeasonDto.java b/api/src/main/java/pro/beerpong/api/model/dto/SeasonDto.java index ed50b7bc..b3fa1eca 100644 --- a/api/src/main/java/pro/beerpong/api/model/dto/SeasonDto.java +++ b/api/src/main/java/pro/beerpong/api/model/dto/SeasonDto.java @@ -13,4 +13,5 @@ public class SeasonDto { private ZonedDateTime endDate; private String groupId; private SeasonSettings seasonSettings; + private GroupMemberDto createdBy; } \ No newline at end of file diff --git a/api/src/main/java/pro/beerpong/api/model/dto/SeasonSettingsDto.java b/api/src/main/java/pro/beerpong/api/model/dto/SeasonSettingsDto.java new file mode 100644 index 00000000..a156ad11 --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/model/dto/SeasonSettingsDto.java @@ -0,0 +1,15 @@ +package pro.beerpong.api.model.dto; + +import lombok.Data; +import pro.beerpong.api.util.DailyLeaderboard; +import pro.beerpong.api.util.RankingAlgorithm; + +@Data +public class SeasonSettingsDto { + private Integer minMatchesToQualify; + private Integer minTeamSize; + private Integer maxTeamSize; + private RankingAlgorithm rankingAlgorithm; + private DailyLeaderboard dailyLeaderboard; + private String wakeTime = "00:00"; +} diff --git a/api/src/main/java/pro/beerpong/api/model/dto/SeasonUpdateDto.java b/api/src/main/java/pro/beerpong/api/model/dto/SeasonUpdateDto.java index 3c8e4981..eab1f93a 100644 --- a/api/src/main/java/pro/beerpong/api/model/dto/SeasonUpdateDto.java +++ b/api/src/main/java/pro/beerpong/api/model/dto/SeasonUpdateDto.java @@ -8,5 +8,5 @@ @Data public class SeasonUpdateDto { - private @NotNull SeasonSettings seasonSettings; + private @NotNull SeasonSettingsDto seasonSettings; } diff --git a/api/src/main/java/pro/beerpong/api/model/dto/UserDto.java b/api/src/main/java/pro/beerpong/api/model/dto/UserDto.java new file mode 100644 index 00000000..3d63c534 --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/model/dto/UserDto.java @@ -0,0 +1,9 @@ +package pro.beerpong.api.model.dto; + +import lombok.Data; + +@Data +public class UserDto { + private String id; + private boolean active; +} \ No newline at end of file diff --git a/api/src/main/java/pro/beerpong/api/repository/DeviceRepository.java b/api/src/main/java/pro/beerpong/api/repository/DeviceRepository.java new file mode 100644 index 00000000..8b7735f9 --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/repository/DeviceRepository.java @@ -0,0 +1,8 @@ +package pro.beerpong.api.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import pro.beerpong.api.model.dao.Device; +import pro.beerpong.api.model.dao.User; + +public interface DeviceRepository extends JpaRepository { +} \ No newline at end of file diff --git a/api/src/main/java/pro/beerpong/api/repository/GroupMemberRepository.java b/api/src/main/java/pro/beerpong/api/repository/GroupMemberRepository.java new file mode 100644 index 00000000..cfcef817 --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/repository/GroupMemberRepository.java @@ -0,0 +1,17 @@ +package pro.beerpong.api.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import pro.beerpong.api.model.dao.Asset; +import pro.beerpong.api.model.dao.GroupMember; + +import java.util.List; + +public interface GroupMemberRepository extends JpaRepository { + List findByUserId(String userId); + + boolean existsByUserIdAndGroupId(String userId, String groupId); + + void deleteByUserIdAndGroupId(String userId, String groupId); + + GroupMember findByUserIdAndGroupId(String userId, String groupId); +} \ No newline at end of file diff --git a/api/src/main/java/pro/beerpong/api/repository/UserRepository.java b/api/src/main/java/pro/beerpong/api/repository/UserRepository.java new file mode 100644 index 00000000..4c9a4181 --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/repository/UserRepository.java @@ -0,0 +1,8 @@ +package pro.beerpong.api.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import pro.beerpong.api.model.dao.Asset; +import pro.beerpong.api.model.dao.User; + +public interface UserRepository extends JpaRepository { +} \ No newline at end of file diff --git a/api/src/main/java/pro/beerpong/api/service/AuthService.java b/api/src/main/java/pro/beerpong/api/service/AuthService.java new file mode 100644 index 00000000..6214fb35 --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/service/AuthService.java @@ -0,0 +1,160 @@ +package pro.beerpong.api.service; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import pro.beerpong.api.auth.JwtTokenProvider; +import pro.beerpong.api.mapping.UserMapper; +import pro.beerpong.api.model.dao.Device; +import pro.beerpong.api.model.dao.Group; +import pro.beerpong.api.model.dao.GroupMember; +import pro.beerpong.api.model.dao.User; +import pro.beerpong.api.model.dto.AuthRefreshDto; +import pro.beerpong.api.model.dto.AuthSignupDto; +import pro.beerpong.api.model.dto.AuthTokenDto; +import pro.beerpong.api.model.dto.UserDto; +import pro.beerpong.api.repository.DeviceRepository; +import pro.beerpong.api.repository.GroupMemberRepository; +import pro.beerpong.api.repository.GroupRepository; +import pro.beerpong.api.repository.UserRepository; +import pro.beerpong.api.util.TokenType; + +@Service +@RequiredArgsConstructor +public class AuthService { + private final JwtTokenProvider tokenProvider; + private final UserRepository userRepository; + private final DeviceRepository deviceRepository; + private final UserMapper userMapper; + private final GroupMemberRepository groupMemberRepository; + private final GroupRepository groupRepository; + + public AuthTokenDto registerDevice(AuthSignupDto dto) { + if (dto.getDeviceId() == null || dto.getDeviceId().trim().isEmpty() || dto.getInstallationType() == null) { + return null; + } + + var user = new User(); + user = userRepository.save(user); + + var device = new Device(); + device.setUser(user); + device.setDeviceId(dto.getDeviceId()); + device.setType(dto.getInstallationType()); + + deviceRepository.save(device); + + var refreshToken = tokenProvider.createRefreshToken(user.getId()); + + return this.buildDto(refreshToken, TokenType.REFRESH); + } + + public AuthTokenDto refreshAuth(AuthRefreshDto dto) { + if (dto.getRefreshToken() == null || dto.getRefreshToken().trim().isEmpty()) { + return null; + } + + var claims = tokenProvider.validateToken(dto.getRefreshToken(), "refresh"); + + if (claims == null) { + return null; + } + + var userId = claims.getSubject(); + + if (!userRepository.existsById(userId)) { + return null; + } + + var accessToken = tokenProvider.createAccessToken(userId); + + return this.buildDto(accessToken, TokenType.ACCESS); + } + + public UserDto userFromAccessToken(String token) { + if (token == null || token.trim().isEmpty()) { + return null; + } + + var claims = tokenProvider.validateToken(token, "access"); + + if (claims == null) { + return null; + } + + var userId = claims.getSubject(); + + return userRepository.findById(userId) + .map(userMapper::userToUserDto) + .orElse(null); + } + + public boolean hasAccessToGroup(UserDto user, String groupId) { + var existing = groupMemberRepository.findByUserIdAndGroupId(user.getId(), groupId); + + return existing != null && existing.isActive(); + } + + public GroupMember memberByUser(UserDto user, String groupId) { + return groupMemberRepository.findByUserIdAndGroupId(user.getId(), groupId); + } + + @Transactional + public GroupMember buildFirstGroupMember(UserDto user) { + var groupMember = new GroupMember(); + groupMember.setUser(userMapper.userDtoToUser(user)); + groupMember.setActive(true); + + return groupMember; + } + + @Transactional + public GroupMember joinGroup(UserDto user, String groupId) { + var existing = groupMemberRepository.findByUserIdAndGroupId(user.getId(), groupId); + + if (existing != null) { + if (existing.isActive()) { + return null; + } else { + existing.setActive(true); + + return groupMemberRepository.save(existing); + } + } + + var groupMember = new GroupMember(); + groupMember.setUser(userMapper.userDtoToUser(user)); + groupMember.setGroup(groupRepository.findById(groupId).orElse(null)); + groupMember.setActive(true); + + saveMember(groupMember); + + return groupMember; + } + + public GroupMember saveMember(GroupMember groupMember) { + return groupMemberRepository.save(groupMember); + } + + @Transactional + public boolean leaveGroup(UserDto user, String groupId) { + var groupMember = groupMemberRepository.findByUserIdAndGroupId(user.getId(), groupId); + + if (groupMember != null) { + groupMember.setActive(false); + + saveMember(groupMember); + return true; + } + return false; + } + + private AuthTokenDto buildDto(String token, TokenType type) { + var result = new AuthTokenDto(); + + result.setToken(token); + result.setType(type); + + return result; + } +} diff --git a/api/src/main/java/pro/beerpong/api/service/GroupService.java b/api/src/main/java/pro/beerpong/api/service/GroupService.java index 116a18ee..67b23896 100644 --- a/api/src/main/java/pro/beerpong/api/service/GroupService.java +++ b/api/src/main/java/pro/beerpong/api/service/GroupService.java @@ -6,9 +6,11 @@ import org.springframework.stereotype.Service; import pro.beerpong.api.mapping.GroupMapper; import pro.beerpong.api.model.dao.Group; +import pro.beerpong.api.model.dao.GroupMember; import pro.beerpong.api.model.dao.Season; import pro.beerpong.api.model.dao.SeasonSettings; import pro.beerpong.api.model.dto.*; +import pro.beerpong.api.repository.GroupMemberRepository; import pro.beerpong.api.repository.GroupRepository; import pro.beerpong.api.repository.MatchRepository; import pro.beerpong.api.repository.SeasonRepository; @@ -19,6 +21,7 @@ import java.time.ZonedDateTime; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import static pro.beerpong.api.util.RandomStringGenerator.generateRandomString; @@ -38,8 +41,10 @@ public class GroupService { private final PlayerService playerService; private final RuleMoveService ruleMoveService; private final RuleService ruleService; + private final GroupMemberRepository groupMemberRepository; + private final AuthService authService; - public GroupDto createGroup(GroupCreateDto groupCreateDto) { + public GroupDto createGroup(GroupCreateDto groupCreateDto, UserDto user) { Group group = groupMapper.groupCreateDtoToGroup(groupCreateDto); group.setInviteCode(generateRandomString(GROUP_INVITE_CODE_LENGTH)); group.setCreatedAt(ZonedDateTime.now()); @@ -51,29 +56,50 @@ public GroupDto createGroup(GroupCreateDto groupCreateDto) { return null; } + var groupMember = authService.buildFirstGroupMember(user); + var season = new Season(); season.setStartDate(ZonedDateTime.now()); - season.setSeasonSettings(new SeasonSettings()); + season.setSeasonSettings(SeasonSettings.createDefault()); group.setActiveSeason(season); group = groupRepository.save(group); + groupMember.setGroup(group); + + groupMember = authService.saveMember(groupMember); + + // TODO maybe find way to prevent double group saving. but not that big of a deal + group.setCreatedBy(groupMember); + group = groupRepository.save(group); + + season.setCreatedBy(groupMember); season.setGroupId(group.getId()); seasonRepository.save(season); Group finalGroup = group; + GroupMember finalGroupMember = groupMember; groupCreateDto.getProfileNames().forEach(s -> { var profileDto = new ProfileCreateDto(); profileDto.setName(s); - profileService.createProfile(finalGroup.getId(), profileDto); + profileService.createProfile(finalGroup.getId(), profileDto, finalGroupMember); }); ruleMoveService.createDefaultRuleMoves(group, season); - ruleService.createDefaultRules(season); + ruleService.createDefaultRules(season, groupCreateDto.getSportPreset(), groupMember); return withStats(groupMapper.groupToGroupDto(group)); } + public List findGroupsByUser(UserDto user) { + return groupMemberRepository.findByUserId(user.getId()).stream() + .map(groupMember -> withStats(groupRepository.findById(groupMember.getGroup().getId()) + .map(groupMapper::groupToGroupDto) + .orElse(null))) + .filter(Objects::nonNull) + .toList(); + } + public GroupDto findGroupsByInviteCode(String inviteCode) { return withStats(groupRepository.findByInviteCode(inviteCode) .map(groupMapper::groupToGroupDto) @@ -97,6 +123,10 @@ public GroupDto getRawGroupById(String id) { .orElse(null); } + public Group getDaoById(String id) { + return groupRepository.findById(id).orElse(null); + } + public GroupDto updateGroup(String id, GroupCreateDto groupCreateDto) { return groupRepository.findById(id) .map(existingGroup -> { diff --git a/api/src/main/java/pro/beerpong/api/service/LeaderboardService.java b/api/src/main/java/pro/beerpong/api/service/LeaderboardService.java index cff7636c..5c06dcba 100644 --- a/api/src/main/java/pro/beerpong/api/service/LeaderboardService.java +++ b/api/src/main/java/pro/beerpong/api/service/LeaderboardService.java @@ -115,7 +115,7 @@ public LeaderboardDto generateLeaderboard(GroupDto group, String scope, boolean } else if (season.getSeasonSettings().getDailyLeaderboard() == DailyLeaderboard.RESET_AT_MIDNIGHT) { startedAt = ZonedDateTime.now().withHour(0).withMinute(0).withSecond(0).withNano(0); } else { - startedAt = matchService.getWakeTime(ZonedDateTime.now(), season.getSeasonSettings().getWakeTimeHour()); + startedAt = matchService.getWakeTime(ZonedDateTime.now(), season.getSeasonSettings().getWakeTime()); } } default -> { diff --git a/api/src/main/java/pro/beerpong/api/service/MatchMoveService.java b/api/src/main/java/pro/beerpong/api/service/MatchMoveService.java index 90b8f77d..b0abfbec 100644 --- a/api/src/main/java/pro/beerpong/api/service/MatchMoveService.java +++ b/api/src/main/java/pro/beerpong/api/service/MatchMoveService.java @@ -33,6 +33,10 @@ public MatchMoveService(MatchMoveRepository matchMoveRepository, public void createMatchMoves(TeamMember teamMember, List moves) { for (MatchMoveDto moveDto : moves) { + if (moveDto.getCount() < 1) { + continue; + } + RuleMove ruleMove = ruleMoveRepository.findById(moveDto.getMoveId()) .orElseThrow(() -> new IllegalArgumentException("RuleMove not found: " + moveDto.getMoveId())); if (!Objects.equals(ruleMove.getSeason().getId(), teamMember.getTeam().getMatch().getSeason().getId())) { diff --git a/api/src/main/java/pro/beerpong/api/service/MatchService.java b/api/src/main/java/pro/beerpong/api/service/MatchService.java index 721c51f2..5e366844 100644 --- a/api/src/main/java/pro/beerpong/api/service/MatchService.java +++ b/api/src/main/java/pro/beerpong/api/service/MatchService.java @@ -6,9 +6,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; +import pro.beerpong.api.mapping.GroupMemberMapper; import pro.beerpong.api.mapping.MatchMoveMapper; import pro.beerpong.api.mapping.PlayerMapper; import pro.beerpong.api.mapping.TeamMapper; +import pro.beerpong.api.mapping.SeasonMapper; import pro.beerpong.api.model.dao.*; import pro.beerpong.api.model.dto.*; import pro.beerpong.api.repository.*; @@ -18,13 +20,14 @@ import pro.beerpong.api.util.AssetType; import java.time.Duration; +import java.time.LocalTime; import java.time.ZonedDateTime; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.function.Predicate; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -53,24 +56,27 @@ public class MatchService { private final TeamService teamService; private final RuleMoveService ruleMoveService; private final PlayerMapper playerMapper; + private final AuthService authService; + private final GroupMemberMapper groupMemberMapper; + private final SeasonMapper seasonMapper; private final AssetService assetService; private final TeamMapper teamMapper; @Autowired public MatchService(SubscriptionHandler subscriptionHandler, - MatchRepository matchRepository, - TeamMemberService teamMemberService, - PlayerRepository playerRepository, - MatchMoveService matchMoveService, - TeamRepository teamRepository, - TeamMemberRepository teamMemberRepository, - MatchMoveRepository matchMoveRepository, - RuleMoveRepository ruleMoveRepository, - MatchMoveMapper matchMoveMapper, - TeamService teamService, - SeasonRepository seasonRepository, - RuleMoveService ruleMoveService, - PlayerMapper playerMapper, AssetService assetService, TeamMapper teamMapper) { + MatchRepository matchRepository, + TeamMemberService teamMemberService, + PlayerRepository playerRepository, + MatchMoveService matchMoveService, + TeamRepository teamRepository, + TeamMemberRepository teamMemberRepository, + MatchMoveRepository matchMoveRepository, + RuleMoveRepository ruleMoveRepository, + MatchMoveMapper matchMoveMapper, + TeamService teamService, + SeasonRepository seasonRepository, + RuleMoveService ruleMoveService, + PlayerMapper playerMapper, AssetService assetService, TeamMapper teamMapper, AuthService authService, GroupMemberMapper groupMemberMapper, SeasonMapper seasonMapper) { this.subscriptionHandler = subscriptionHandler; this.matchRepository = matchRepository; @@ -87,37 +93,49 @@ public MatchService(SubscriptionHandler subscriptionHandler, this.teamService = teamService; this.ruleMoveService = ruleMoveService; this.playerMapper = playerMapper; + this.authService = authService; + this.groupMemberMapper = groupMemberMapper; + this.seasonMapper = seasonMapper; this.assetService = assetService; this.teamMapper = teamMapper; } public boolean invalidCreateDto(String groupId, String seasonId, MatchCreateDto dto) { - return !dto.getTeams().stream() - .allMatch(teamCreateDto -> teamCreateDto.getTeamMembers().stream().allMatch(memberDto -> { - var player = playerRepository.findById(memberDto.getPlayerId()); + var playerIds = dto.getTeams().stream() + .flatMap(teamCreateDto -> teamCreateDto.getTeamMembers().stream().map(TeamMemberCreateDto::getPlayerId)) + .toList(); - if (player.isEmpty() || !player.get().getSeason().getId().equals(seasonId) - || !player.get().getSeason().getGroupId().equals(groupId)) { - return false; - } + var finishMoves = dto.getTeams().stream() + .flatMap(teamCreateDto -> teamCreateDto.getTeamMembers().stream()) + .flatMap(memberCreateDto -> memberCreateDto.getMoves().stream()) + .filter(matchMoveDto -> ruleMoveService.isFinish(matchMoveDto.getMoveId()) && matchMoveDto.getCount() > 0) + .toList(); - return memberDto.getMoves().stream().allMatch(matchMoveDto -> { - var move = ruleMoveRepository.findById(matchMoveDto.getMoveId()); - - return move.isPresent() && move.get().getSeason().getId().equals(seasonId) - && move.get().getSeason().getGroupId().equals(groupId); - }); - })) || - dto.getTeams().stream() - .flatMap(teamCreateDto -> teamCreateDto.getTeamMembers().stream()) - .flatMap(memberCreateDto -> memberCreateDto.getMoves().stream()) - .filter(matchMoveDto -> ruleMoveService.isFinish(matchMoveDto.getMoveId()) - && matchMoveDto.getCount() == 1) - .count() != 1; + return playerIds.stream().distinct().count() != playerIds.size() || + finishMoves.size() != 1 || + finishMoves.getFirst().getCount() != 1 || + !dto.getTeams().stream().allMatch(teamCreateDto -> + teamCreateDto.getTeamMembers().stream().allMatch(memberDto -> { + var player = playerRepository.findById(memberDto.getPlayerId()); + + if (player.isEmpty() || !player.get().getSeason().getId().equals(seasonId) || !player.get().getSeason().getGroupId().equals(groupId)) { + return false; + } + if (player.isEmpty() || !player.get().getSeason().getId().equals(seasonId) + || !player.get().getSeason().getGroupId().equals(groupId)) { + return false; + } + + return memberDto.getMoves().stream().allMatch(matchMoveDto -> { + var move = ruleMoveRepository.findById(matchMoveDto.getMoveId()); + + return move.isPresent() && move.get().getSeason().getId().equals(seasonId) && move.get().getSeason().getGroupId().equals(groupId); + }); + })); } @Transactional - public MatchDto createNewMatch(@NotNull Group group, @NotNull Season season, MatchCreateDto matchCreateDto) { + public MatchDto createNewMatch(@NotNull Group group, @NotNull Season season, MatchCreateDto matchCreateDto, UserDto user) { if (!group.getActiveSeason().getId().equals(season.getId()) || invalidCreateDto(group.getId(), season.getId(), matchCreateDto)) { return null; @@ -127,6 +145,7 @@ public MatchDto createNewMatch(@NotNull Group group, @NotNull Season season, Mat match.setDate(ZonedDateTime.now()); match.setSeason(season); + match.setCreatedBy(authService.memberByUser(user, group.getId())); match = matchRepository.save(match); @@ -216,14 +235,14 @@ public long numOfMatchesInPastSeasons(GroupDto group) { .reduce(0L, Long::sum); } - public Stream streamAllMatchesToday(GroupDto group, Season season) { + public Stream streamAllMatchesToday(GroupDto group, SeasonDto season) { var now = ZonedDateTime.now(); Predicate predicate = switch (season.getSeasonSettings().getDailyLeaderboard()) { case WAKE_TIME -> - match -> match.getDate().isAfter(getWakeTime(now, season.getSeasonSettings().getWakeTimeHour())); - case LAST_24_HOURS -> (match) -> !match.getDate().isAfter(now) - && Duration.between(match.getDate(), now).toMinutes() < MINUTES_IN_DAY; + match -> match.getDate().isAfter(getWakeTime(now, season.getSeasonSettings().getWakeTime())); + case LAST_24_HOURS -> + (match) -> !match.getDate().isAfter(now) && Duration.between(match.getDate(), now).toMinutes() < MINUTES_IN_DAY; case RESET_AT_MIDNIGHT -> (match) -> match.getDate().toLocalDate().equals(now.toLocalDate()); }; @@ -241,8 +260,8 @@ public Stream streamAllMatchesToday(GroupDto group, Season season) { } } - public ZonedDateTime getWakeTime(ZonedDateTime now, int wakeTimeHour) { - var wakeTimeToday = now.withHour(wakeTimeHour).withMinute(0).withSecond(0).withNano(0); + public ZonedDateTime getWakeTime(ZonedDateTime now, LocalTime wakeTime) { + var wakeTimeToday = now.withHour(wakeTime.getHour()).withMinute(wakeTime.getMinute()).withSecond(0).withNano(0); if (now.isBefore(wakeTimeToday)) { wakeTimeToday = wakeTimeToday.minusDays(1); @@ -398,7 +417,7 @@ public ErrorCodes deleteMatch(String id, String seasonId, String groupId) { teamRepository.deleteAllById(match.getTeams().stream().map(TeamDto::getId).toList()); matchRepository.deleteById(id); } else { - error.set(ErrorCodes.PLAYER_VALIDATION_FAILED); + error.set(ErrorCodes.MATCH_GROUP_OR_SEASON_ID_DONT_MATCH); } } else { error.set(ErrorCodes.SEASON_ALREADY_ENDED); @@ -413,7 +432,8 @@ private MatchDto matchToEmptyMatchDto(Match match) { dto.setId(match.getId()); dto.setDate(match.getDate()); - dto.setSeason(match.getSeason()); + dto.setSeason(seasonMapper.seasonToSeasonDto(match.getSeason())); + dto.setCreatedBy(groupMemberMapper.groupMemberToGroupMemberDto(match.getCreatedBy())); return dto; } @@ -423,7 +443,8 @@ private MatchDto matchToMatchDto(Match match) { dto.setId(match.getId()); dto.setDate(match.getDate()); - dto.setSeason(match.getSeason()); + dto.setSeason(seasonMapper.seasonToSeasonDto(match.getSeason())); + dto.setCreatedBy(groupMemberMapper.groupMemberToGroupMemberDto(match.getCreatedBy())); loadMatchInfo(match, dto); diff --git a/api/src/main/java/pro/beerpong/api/service/ProfileService.java b/api/src/main/java/pro/beerpong/api/service/ProfileService.java index c4863cd4..093bcdf0 100644 --- a/api/src/main/java/pro/beerpong/api/service/ProfileService.java +++ b/api/src/main/java/pro/beerpong/api/service/ProfileService.java @@ -6,9 +6,14 @@ import org.springframework.stereotype.Service; import pro.beerpong.api.mapping.GroupMapper; import pro.beerpong.api.mapping.ProfileMapper; +import pro.beerpong.api.model.dao.GroupMember; import pro.beerpong.api.model.dao.Profile; import pro.beerpong.api.model.dao.Season; import pro.beerpong.api.model.dto.*; +import pro.beerpong.api.model.dto.ProfileCreateDto; +import pro.beerpong.api.model.dto.ProfileCreatedDto; +import pro.beerpong.api.model.dto.ProfileDto; +import pro.beerpong.api.model.dto.UserDto; import pro.beerpong.api.repository.GroupRepository; import pro.beerpong.api.repository.ProfileRepository; import pro.beerpong.api.util.AssetType; @@ -25,8 +30,9 @@ public class ProfileService { private final GroupMapper groupMapper; private final ProfileMapper profileMapper; private final PlayerService playerService; + private final AuthService authService; - public ProfileCreatedDto createPlayer(String groupId, ProfileCreateDto dto) { + public ProfileCreatedDto createPlayer(String groupId, ProfileCreateDto dto, UserDto user) { var existing = this.getProfileByName(groupId, dto.getName()); if (existing != null) { @@ -63,19 +69,20 @@ public ProfileCreatedDto createPlayer(String groupId, ProfileCreateDto dto) { return new ProfileCreatedDto(existing, false, (lastPlayer != null ? lastPlayer.getSeason().getId() : null)); } } else { - return new ProfileCreatedDto(this.createProfile(groupId, dto), false, null); + return new ProfileCreatedDto(this.createProfile(groupId, dto, authService.memberByUser(user, groupId)), false, null); } } - public ProfileDto createProfile(String groupId, ProfileCreateDto profileCreateDto) { - return this.createProfile(groupId, profileCreateDto, true); + public ProfileDto createProfile(String groupId, ProfileCreateDto profileCreateDto, GroupMember groupMember) { + return this.createProfile(groupId, profileCreateDto, true, groupMember); } - public ProfileDto createProfile(String groupId, ProfileCreateDto profileCreateDto, boolean createPlayer) { + public ProfileDto createProfile(String groupId, ProfileCreateDto profileCreateDto, boolean createPlayer, GroupMember groupMember) { var groupOptional = groupRepository.findById(groupId); var profile = profileMapper.profileCreateDtoToProfile(profileCreateDto); profile.setGroup(groupOptional.orElseThrow()); + profile.setCreatedBy(groupMember); var savedProfile = profileRepository.save(profile); @@ -125,10 +132,10 @@ public boolean deleteProfile(String id) { return false; } - public ProfileDto updateProfile(String id, ProfileCreateDto profileCreateDto) { + public ProfileDto updateProfile(String id, String groupId, ProfileCreateDto profileCreateDto) { var profile = getRawProfileById(id); - if (profile == null) { + if (profile == null || !profile.getGroup().getId().equals(groupId)) { return null; } diff --git a/api/src/main/java/pro/beerpong/api/service/RuleMoveService.java b/api/src/main/java/pro/beerpong/api/service/RuleMoveService.java index a3d6da87..40f5473a 100644 --- a/api/src/main/java/pro/beerpong/api/service/RuleMoveService.java +++ b/api/src/main/java/pro/beerpong/api/service/RuleMoveService.java @@ -21,7 +21,7 @@ @Service public class RuleMoveService { - private static final List DEFAULT_BEERPONG_MOVES = List.of( + public static final List DEFAULT_BEERPONG_MOVES = List.of( buildRuleMove("Normal", 1, 0, false), buildRuleMove("Bomb", 2, 0, false), buildRuleMove("Bouncer", 2, 0, false), @@ -31,7 +31,7 @@ public class RuleMoveService { buildRuleMove("Finish - Ring of fire", 1, 10, true) ); - private static final List DEFAULT_MOVES = List.of( + public static final List DEFAULT_MOVES = List.of( buildRuleMove("Normal", 1, 0, false), buildRuleMove("Finish - Normal", 1, 3, true) ); @@ -129,7 +129,7 @@ public void copyRuleMovesFromOldSeason(Season oldSeason, Season newSeason) { } public void createDefaultRuleMoves(Group group, Season season) { - Stream ruleMoves; + Stream ruleMoves; if (group.getSportPreset() != null && group.getSportPreset().equals(GroupPresetsController.BEERPONG.getId())) { ruleMoves = DEFAULT_BEERPONG_MOVES.stream(); @@ -138,15 +138,19 @@ public void createDefaultRuleMoves(Group group, Season season) { } ruleMoves.map(ruleMove -> { - var move = ruleMove.clone(); + var move = new RuleMove(); + move.setName(ruleMove.getName()); + move.setFinishingMove(ruleMove.isFinishingMove()); + move.setPointsForScorer(ruleMove.getPointsForScorer()); + move.setPointsForTeam(ruleMove.getPointsForTeam()); move.setSeason(season); return move; }) .forEach(moveRepository::save); } - private static RuleMove buildRuleMove(String name, int pointsForScorer, int pointsForTeam, boolean finish) { - var ruleMove = new RuleMove(); + private static RuleMoveDto buildRuleMove(String name, int pointsForScorer, int pointsForTeam, boolean finish) { + var ruleMove = new RuleMoveDto(); ruleMove.setName(name); ruleMove.setPointsForScorer(pointsForScorer); diff --git a/api/src/main/java/pro/beerpong/api/service/RuleService.java b/api/src/main/java/pro/beerpong/api/service/RuleService.java index 62e03841..482d01a2 100644 --- a/api/src/main/java/pro/beerpong/api/service/RuleService.java +++ b/api/src/main/java/pro/beerpong/api/service/RuleService.java @@ -3,11 +3,14 @@ import jakarta.transaction.Transactional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import pro.beerpong.api.control.GroupPresetsController; import pro.beerpong.api.mapping.RuleMapper; +import pro.beerpong.api.model.dao.GroupMember; import pro.beerpong.api.model.dao.Rule; import pro.beerpong.api.model.dao.Season; import pro.beerpong.api.model.dto.RuleCreateDto; import pro.beerpong.api.model.dto.RuleDto; +import pro.beerpong.api.model.dto.UserDto; import pro.beerpong.api.repository.RuleRepository; import pro.beerpong.api.sockets.SocketEvent; import pro.beerpong.api.sockets.SocketEventData; @@ -17,7 +20,7 @@ @Service public class RuleService { - private static final List DEFAULT_RULES = List.of( + public static final List DEFAULT_RULES = List.of( buildRule("Teams", "The two teams can have any size, and they don't have to have the same number of players."), buildRule("Cup Setup", "Ten cups per side are to be arranged in a pyramid pointing towards the opponent. The back row must be no further from the table edge than one cup diameter. All cups are to be filled with the same amount of liquid, preferably halfway full."), buildRule("Number of Balls", "Each side throws at least two balls. If there are three or more players per side, increase the ball count by one per extra player."), @@ -43,22 +46,27 @@ public class RuleService { private final RuleRepository ruleRepository; private final RuleMapper ruleMapper; + private final AuthService authService; @Autowired - public RuleService(SubscriptionHandler subscriptionHandler, RuleRepository matchRepository, RuleMapper ruleMapper) { + public RuleService(SubscriptionHandler subscriptionHandler, RuleRepository matchRepository, RuleMapper ruleMapper, AuthService authService) { this.subscriptionHandler = subscriptionHandler; this.ruleRepository = matchRepository; this.ruleMapper = ruleMapper; + this.authService = authService; } @Transactional - public List writeRules(String groupId, Season season, List rules) { + public List writeRules(String groupId, Season season, List rules, UserDto user) { ruleRepository.deleteBySeasonId(season.getId()); + var createdBy = authService.memberByUser(user, groupId); + return rules.stream() .map(dto -> { var rule = ruleMapper.ruleCreateDtoToRule(dto); rule.setSeason(season); + rule.setCreatedBy(createdBy); return rule; }) .filter(dto -> dto.getSeason().getId().equals(season.getId()) && @@ -73,13 +81,14 @@ public void copyRulesFromOldSeason(Season oldSeason, Season newSeason) { } ruleRepository.findBySeasonId(oldSeason.getId()).forEach(oldRule -> { - var ruleMove = new Rule(); + var rule = new Rule(); - ruleMove.setTitle(oldRule.getTitle()); - ruleMove.setDescription(oldRule.getDescription()); - ruleMove.setSeason(newSeason); + rule.setTitle(oldRule.getTitle()); + rule.setDescription(oldRule.getDescription()); + rule.setSeason(newSeason); + rule.setCreatedBy(oldRule.getCreatedBy()); - ruleRepository.save(ruleMove); + ruleRepository.save(rule); }); } @@ -90,18 +99,23 @@ public List getAllRules(String seasonId) { .toList(); } - public void createDefaultRules(Season season) { - DEFAULT_RULES.stream() - .map(rule -> { - var rle = rule.clone(); - rle.setSeason(season); - return rle; - }) - .forEach(ruleRepository::save); + public void createDefaultRules(Season season, String sportPreset, GroupMember createdBy) { + if (sportPreset != null && sportPreset.equals(GroupPresetsController.BEERPONG.getId())) { + DEFAULT_RULES.stream() + .map(rule -> { + var rle = new Rule(); + rle.setTitle(rule.getTitle()); + rle.setDescription(rule.getDescription()); + rle.setSeason(season); + rle.setCreatedBy(createdBy); + return rle; + }) + .forEach(ruleRepository::save); + } } - private static Rule buildRule(String title, String description) { - var rule = new Rule(); + private static RuleDto buildRule(String title, String description) { + var rule = new RuleDto(); rule.setTitle(title); rule.setDescription(description); diff --git a/api/src/main/java/pro/beerpong/api/service/SeasonService.java b/api/src/main/java/pro/beerpong/api/service/SeasonService.java index f8aee932..f21b1d22 100644 --- a/api/src/main/java/pro/beerpong/api/service/SeasonService.java +++ b/api/src/main/java/pro/beerpong/api/service/SeasonService.java @@ -1,21 +1,25 @@ package pro.beerpong.api.service; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import pro.beerpong.api.mapping.*; -import pro.beerpong.api.model.dao.*; +import pro.beerpong.api.model.dao.Group; +import pro.beerpong.api.model.dao.Player; +import pro.beerpong.api.model.dao.Season; +import pro.beerpong.api.model.dao.SeasonSettings; import pro.beerpong.api.model.dto.*; import pro.beerpong.api.repository.GroupRepository; import pro.beerpong.api.repository.PlayerRepository; import pro.beerpong.api.repository.PlayerStatisticsRepository; import pro.beerpong.api.repository.SeasonRepository; +import pro.beerpong.api.sockets.LocalTimeAdapter; import pro.beerpong.api.sockets.SocketEvent; import pro.beerpong.api.sockets.SocketEventData; import pro.beerpong.api.sockets.SubscriptionHandler; import pro.beerpong.api.util.NullablePair; +import java.time.LocalTime; import java.time.ZonedDateTime; import java.util.List; import java.util.Optional; @@ -37,6 +41,8 @@ public class SeasonService { private final ProfileMapper profileMapper; private final PlayerStatisticsMapper playerStatisticsMapper; private final GroupService groupService; + private final SeasonSettingsMapper seasonSettingsMapper; + private final AuthService authService; @Autowired public SeasonService(SubscriptionHandler subscriptionHandler, @@ -45,7 +51,7 @@ public SeasonService(SubscriptionHandler subscriptionHandler, PlayerService playerService, RuleMoveService ruleMoveService, RuleService ruleService, - SeasonMapper seasonMapper, LeaderboardService leaderboardService, GroupMapper groupMapper, PlayerMapper playerMapper, PlayerStatisticsRepository playerStatisticsRepository, PlayerRepository playerRepository, ProfileMapper profileMapper, PlayerStatisticsMapper playerStatisticsMapper, GroupService groupService) { + SeasonMapper seasonMapper, LeaderboardService leaderboardService, GroupMapper groupMapper, PlayerMapper playerMapper, PlayerStatisticsRepository playerStatisticsRepository, PlayerRepository playerRepository, ProfileMapper profileMapper, PlayerStatisticsMapper playerStatisticsMapper, GroupService groupService, SeasonSettingsMapper seasonSettingsMapper, AuthService authService) { this.subscriptionHandler = subscriptionHandler; this.seasonRepository = seasonRepository; this.groupRepository = groupRepository; @@ -61,9 +67,11 @@ public SeasonService(SubscriptionHandler subscriptionHandler, this.profileMapper = profileMapper; this.playerStatisticsMapper = playerStatisticsMapper; this.groupService = groupService; + this.seasonSettingsMapper = seasonSettingsMapper; + this.authService = authService; } - public SeasonDto startNewSeason(SeasonCreateDto dto, String groupId) { + public SeasonDto startNewSeason(SeasonCreateDto dto, String groupId, UserDto user) { var groupOptional = groupRepository.findById(groupId); if (groupOptional.isEmpty()) { @@ -76,7 +84,8 @@ public SeasonDto startNewSeason(SeasonCreateDto dto, String groupId) { newSeason.setStartDate(ZonedDateTime.now()); newSeason.setGroupId(groupOptional.get().getId()); - newSeason.setSeasonSettings(new SeasonSettings()); + newSeason.setSeasonSettings(SeasonSettings.createDefault()); + newSeason.setCreatedBy(authService.memberByUser(user, groupId)); if (oldSeason != null && oldSeason.getSeasonSettings() != null) { newSeason.getSeasonSettings().setMaxTeamSize(oldSeason.getSeasonSettings().getMaxTeamSize()); @@ -84,7 +93,7 @@ public SeasonDto startNewSeason(SeasonCreateDto dto, String groupId) { newSeason.getSeasonSettings().setMinMatchesToQualify(oldSeason.getSeasonSettings().getMinMatchesToQualify()); newSeason.getSeasonSettings().setRankingAlgorithm(oldSeason.getSeasonSettings().getRankingAlgorithm()); newSeason.getSeasonSettings().setDailyLeaderboard(oldSeason.getSeasonSettings().getDailyLeaderboard()); - newSeason.getSeasonSettings().setWakeTimeHour(oldSeason.getSeasonSettings().getWakeTimeHour()); + newSeason.getSeasonSettings().setWakeTime(oldSeason.getSeasonSettings().getWakeTime()); } var season = seasonRepository.save(newSeason); @@ -133,16 +142,15 @@ public SeasonDto startNewSeason(SeasonCreateDto dto, String groupId) { return newDto; } - public List calcStatsForPlayersInSeason(String seasonId, boolean showInactive, boolean showStats) { - var players = playerService.getBySeasonId(seasonId, showInactive); - var season = seasonRepository.findById(seasonId).orElse(null); + public List calcStatsForPlayersInSeason(SeasonDto season, boolean showInactive, boolean showStats) { + var players = playerService.getBySeasonId(season.getId(), showInactive); - if (showStats && season != null) { + if (showStats) { return leaderboardService.generateLeaderboard( - groupService.getRawGroupById(season.getGroupId()), + groupService.getRawGroupById(season.getGroupId()), "season", true, - seasonId, + season.getId(), players.stream() ) .getEntries(); @@ -157,16 +165,21 @@ public SeasonDto updateSeason(Season season, SeasonUpdateDto dto) { return Optional.ofNullable(season) .map(existingSeason -> { if (existingSeason.getSeasonSettings() == null) { - dto.getSeasonSettings().setId(null); - existingSeason.setSeasonSettings(dto.getSeasonSettings()); - } else { - existingSeason.getSeasonSettings().setMaxTeamSize(dto.getSeasonSettings().getMaxTeamSize()); - existingSeason.getSeasonSettings().setMinTeamSize(dto.getSeasonSettings().getMinTeamSize()); + existingSeason.setSeasonSettings(SeasonSettings.createDefault()); + } + + if (dto.getSeasonSettings().getMinMatchesToQualify() != null) existingSeason.getSeasonSettings().setMinMatchesToQualify(dto.getSeasonSettings().getMinMatchesToQualify()); - existingSeason.getSeasonSettings().setRankingAlgorithm(dto.getSeasonSettings().getRankingAlgorithm()); + if (dto.getSeasonSettings().getMinTeamSize() != null) + existingSeason.getSeasonSettings().setMinTeamSize(dto.getSeasonSettings().getMinTeamSize()); + if (dto.getSeasonSettings().getMaxTeamSize() != null) + existingSeason.getSeasonSettings().setMaxTeamSize(dto.getSeasonSettings().getMaxTeamSize()); + if (dto.getSeasonSettings().getWakeTime() != null) + existingSeason.getSeasonSettings().setWakeTime(LocalTime.parse(dto.getSeasonSettings().getWakeTime(), LocalTimeAdapter.FORMATTER)); + if (dto.getSeasonSettings().getDailyLeaderboard() != null) existingSeason.getSeasonSettings().setDailyLeaderboard(dto.getSeasonSettings().getDailyLeaderboard()); - existingSeason.getSeasonSettings().setWakeTimeHour(dto.getSeasonSettings().getWakeTimeHour()); - } + if (dto.getSeasonSettings().getRankingAlgorithm() != null) + existingSeason.getSeasonSettings().setRankingAlgorithm(dto.getSeasonSettings().getRankingAlgorithm()); var seasonDto = seasonMapper.seasonToSeasonDto(seasonRepository.save(existingSeason)); diff --git a/api/src/main/java/pro/beerpong/api/sockets/LocalTimeAdapter.java b/api/src/main/java/pro/beerpong/api/sockets/LocalTimeAdapter.java index 96f6f217..a5e77ddc 100644 --- a/api/src/main/java/pro/beerpong/api/sockets/LocalTimeAdapter.java +++ b/api/src/main/java/pro/beerpong/api/sockets/LocalTimeAdapter.java @@ -10,25 +10,25 @@ import java.time.format.DateTimeFormatter; public class LocalTimeAdapter extends TypeAdapter { - public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); + public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); - @Override - public void write(JsonWriter out, LocalTime value) throws IOException { - if (value == null) { - out.nullValue(); - } else { - out.value(value.format(FORMATTER)); + @Override + public void write(JsonWriter out, LocalTime value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.format(FORMATTER)); + } } - } - @Override - public LocalTime read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } else { - String timeString = in.nextString(); - return LocalTime.parse(timeString, FORMATTER); + @Override + public LocalTime read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } else { + String timeString = in.nextString(); + return LocalTime.parse(timeString, FORMATTER); + } } - } } \ No newline at end of file diff --git a/api/src/main/java/pro/beerpong/api/sockets/ZonedDateTimeAdapter.java b/api/src/main/java/pro/beerpong/api/sockets/ZonedDateTimeAdapter.java index 74af55dc..b6b0200f 100644 --- a/api/src/main/java/pro/beerpong/api/sockets/ZonedDateTimeAdapter.java +++ b/api/src/main/java/pro/beerpong/api/sockets/ZonedDateTimeAdapter.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; public class ZonedDateTimeAdapter extends TypeAdapter { @@ -29,7 +30,11 @@ public ZonedDateTime read(JsonReader in) throws IOException { return null; } else { String dateTimeString = in.nextString(); - return ZonedDateTime.parse(dateTimeString, formatter); + try { + return ZonedDateTime.parse(dateTimeString, formatter); + } catch (DateTimeParseException e) { + return null; + } } } } diff --git a/api/src/main/java/pro/beerpong/api/util/InstallationType.java b/api/src/main/java/pro/beerpong/api/util/InstallationType.java new file mode 100644 index 00000000..ef37b57d --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/util/InstallationType.java @@ -0,0 +1,6 @@ +package pro.beerpong.api.util; + +public enum InstallationType { + IOS, + ANDROID +} diff --git a/api/src/main/java/pro/beerpong/api/util/TokenType.java b/api/src/main/java/pro/beerpong/api/util/TokenType.java new file mode 100644 index 00000000..b9e8faf8 --- /dev/null +++ b/api/src/main/java/pro/beerpong/api/util/TokenType.java @@ -0,0 +1,6 @@ +package pro.beerpong.api.util; + +public enum TokenType { + ACCESS, + REFRESH +} diff --git a/api/src/main/resources/application-test.properties b/api/src/main/resources/application-test.properties new file mode 100644 index 00000000..4d7749e8 --- /dev/null +++ b/api/src/main/resources/application-test.properties @@ -0,0 +1 @@ +sentry.enabled=false \ No newline at end of file diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties new file mode 100644 index 00000000..a65adddc --- /dev/null +++ b/api/src/main/resources/application.properties @@ -0,0 +1,2 @@ +jwt.secret=${JWT_SECRET} +jwt.access.expiration-ms=3600000 \ No newline at end of file diff --git a/api/src/test/java/pro/beerpong/api/CreateMatchTest.java b/api/src/test/java/pro/beerpong/api/CreateMatchTest.java deleted file mode 100644 index 5cc8eac0..00000000 --- a/api/src/test/java/pro/beerpong/api/CreateMatchTest.java +++ /dev/null @@ -1,320 +0,0 @@ -package pro.beerpong.api; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.test.context.ActiveProfiles; -import pro.beerpong.api.control.GroupPresetsController; -import pro.beerpong.api.model.dto.*; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@ActiveProfiles("test") -public class CreateMatchTest { - @LocalServerPort - private int port; - - @Autowired - private TestUtils testUtils; - -// @BeforeEach -// void setUp() { -// if (seasonRepository.findById(seasonId).isEmpty()) { -// var season = new pro.beerpong.api.model.dao.Season(); -// season.setId(seasonId); -// season.setGroupId(groupId); -// seasonRepository.save(season); -// } -// } - -// @Test -// @SuppressWarnings("unchecked") -// void createMatch_withTeamsMembersAndMoves_shouldCreateMatchTeamsAndMoves() throws Exception { -// var createGroupDto = new GroupCreateDto(); -// createGroupDto.setProfileNames(List.of("player1", "player2", "player3", "player4")); -// createGroupDto.setName("test"); -// createGroupDto.setSportPreset("beerpong"); -// -// var prerequisiteGroupResponse = testUtils.performPost(port, "/groups", createGroupDto, GroupDto.class); -// -// assertNotNull(prerequisiteGroupResponse); -// assertEquals(200, prerequisiteGroupResponse.getStatusCode().value()); -// -// ResponseEnvelope prerequisiteEnvelope = (ResponseEnvelope) prerequisiteGroupResponse.getBody(); -// assertNotNull(prerequisiteEnvelope); -// assertEquals(ResponseEnvelope.Status.OK, prerequisiteEnvelope.getStatus()); -// assertNull(prerequisiteEnvelope.getError()); -// assertEquals(200, prerequisiteEnvelope.getHttpCode()); -// -// var prerequisiteGroup = prerequisiteEnvelope.getData(); -// assertEquals(GroupPresetsController.BEERPONG.getId(), prerequisiteGroup.getSportPreset().getId()); -// var response = testUtils.performGet(port, "/groups?inviteCode=" + prerequisiteGroup.getInviteCode(), GroupDto.class); -// -// assertNotNull(response); -// assertEquals(200, response.getStatusCode().value()); -// -// ResponseEnvelope envelope = (ResponseEnvelope) response.getBody(); -// assertNotNull(envelope); -// assertEquals(ResponseEnvelope.Status.OK, envelope.getStatus()); -// assertNull(envelope.getError()); -// assertEquals(200, envelope.getHttpCode()); -// -// var group = envelope.getData(); -// // if this is not here, the startDate millis are rounded and this test fails -// group.getActiveSeason().setStartDate(prerequisiteGroup.getActiveSeason().getStartDate()); -// group.setCreatedAt(prerequisiteGroup.getCreatedAt()); -// -// assertNotNull(group); -// assertNotNull(group.getName()); -// assertEquals(prerequisiteGroup, group); -// assertNotNull(group.getId()); -// assertNotNull(group.getInviteCode()); -// assertNotNull(group.getActiveSeason()); -// assertNotNull(group.getActiveSeason().getId()); -// assertEquals(group.getActiveSeason().getGroupId(), group.getId()); -// -// var season = group.getActiveSeason(); -// -// var playersResponse = testUtils.performGet(port, "/groups/" + group.getId() + "/seasons/" + season.getId() + "/players", List.class, PlayerDto.class); -// -// assertNotNull(playersResponse); -// assertEquals(200, playersResponse.getStatusCode().value()); -// -// ResponseEnvelope> playersEnvelope = (ResponseEnvelope>) playersResponse.getBody(); -// assertNotNull(playersEnvelope); -// assertEquals(ResponseEnvelope.Status.OK, playersEnvelope.getStatus()); -// assertNull(playersEnvelope.getError()); -// assertEquals(200, playersEnvelope.getHttpCode()); -// -// var players = playersEnvelope.getData(); -// -// var createRulemMoveDto = new RuleMoveCreateDto(); -// createRulemMoveDto.setName("RuleMove1"); -// createRulemMoveDto.setFinishingMove(false); -// createRulemMoveDto.setPointsForTeam(0); -// createRulemMoveDto.setPointsForScorer(1); -// var createRulemMoveDto1 = new RuleMoveCreateDto(); -// createRulemMoveDto1.setName("RuleMove2"); -// createRulemMoveDto1.setFinishingMove(true); -// createRulemMoveDto1.setPointsForTeam(1); -// createRulemMoveDto1.setPointsForScorer(1); -// -// var ruleMoveResponse = testUtils.performPost(port, "/groups/" + group.getId() + "/seasons/" + season.getId() + "/rule-moves", createRulemMoveDto, RuleMoveDto.class); -// var ruleMoveResponse1 = testUtils.performPost(port, "/groups/" + group.getId() + "/seasons/" + season.getId() + "/rule-moves", createRulemMoveDto1, RuleMoveDto.class); -// -// assertNotNull(ruleMoveResponse); -// assertNotNull(ruleMoveResponse1); -// assertEquals(200, ruleMoveResponse.getStatusCode().value()); -// assertEquals(200, ruleMoveResponse1.getStatusCode().value()); -// -// ResponseEnvelope ruleMoveEnvelope = (ResponseEnvelope) ruleMoveResponse.getBody(); -// ResponseEnvelope ruleMoveEnvelope1 = (ResponseEnvelope) ruleMoveResponse1.getBody(); -// assertNotNull(ruleMoveEnvelope); -// assertNotNull(ruleMoveEnvelope1); -// assertEquals(ResponseEnvelope.Status.OK, ruleMoveEnvelope.getStatus()); -// assertEquals(ResponseEnvelope.Status.OK, ruleMoveEnvelope1.getStatus()); -// assertNull(ruleMoveEnvelope.getError()); -// assertNull(ruleMoveEnvelope1.getError()); -// assertEquals(200, ruleMoveEnvelope.getHttpCode()); -// assertEquals(200, ruleMoveEnvelope1.getHttpCode()); -// -// var ruleMove = ruleMoveEnvelope.getData(); -// var ruleMove1 = ruleMoveEnvelope1.getData(); -// -// // Arrange: Erstelle ein MatchCreateDto mit Teams, TeamMembers und Moves -// MatchCreateDto matchCreateDto = new MatchCreateDto(); -// -// // Team 1 mit zwei Spielern und ihren Moves -// TeamCreateDto team1 = new TeamCreateDto(); -// TeamMemberCreateDto member1 = new TeamMemberCreateDto(); -// member1.setPlayerId(players.get(0).getId()); -// -// MatchMoveDto move1 = new MatchMoveDto(); -// move1.setMoveId(ruleMove.getId()); -// move1.setCount(3); -// -// MatchMoveDto move2 = new MatchMoveDto(); -// move2.setMoveId(ruleMove.getId()); -// move2.setCount(5); -// -// MatchMoveDto move7 = new MatchMoveDto(); -// move7.setMoveId(ruleMove1.getId()); -// move7.setCount(1); -// -// member1.setMoves(List.of(move1, move2, move7)); -// -// TeamMemberCreateDto member2 = new TeamMemberCreateDto(); -// member2.setPlayerId(players.get(1).getId()); -// -// MatchMoveDto move3 = new MatchMoveDto(); -// move3.setMoveId(ruleMove.getId()); -// move3.setCount(2); -// -// member2.setMoves(List.of(move3)); -// -// team1.setTeamMembers(List.of(member1, member2)); -// -// // Team 2 mit zwei Spielern und ihren Moves -// TeamCreateDto team2 = new TeamCreateDto(); -// TeamMemberCreateDto member3 = new TeamMemberCreateDto(); -// member3.setPlayerId(players.get(2).getId()); -// -// MatchMoveDto move4 = new MatchMoveDto(); -// move4.setMoveId(ruleMove.getId()); -// move4.setCount(4); -// -// member3.setMoves(List.of(move4)); -// -// TeamMemberCreateDto member4 = new TeamMemberCreateDto(); -// member4.setPlayerId(players.get(3).getId()); -// -// MatchMoveDto move5 = new MatchMoveDto(); -// move5.setMoveId(ruleMove.getId()); -// move5.setCount(1); -// -// MatchMoveDto move6 = new MatchMoveDto(); -// move6.setMoveId(ruleMove.getId()); -// move6.setCount(6); -// -// member4.setMoves(List.of(move5, move6)); -// -// team2.setTeamMembers(List.of(member3, member4)); -// -// matchCreateDto.setTeams(List.of(team1, team2)); -// -// var createMatchResponse = testUtils.performPost(port, "/groups/" + group.getId() + "/seasons/" + season.getId() + "/matches", matchCreateDto, MatchDto.class); -// -// assertNotNull(createMatchResponse); -// assertEquals(200, createMatchResponse.getStatusCode().value()); -// -// ResponseEnvelope matchEnvelope = (ResponseEnvelope) createMatchResponse.getBody(); -// assertNotNull(matchEnvelope); -// assertEquals(ResponseEnvelope.Status.OK, matchEnvelope.getStatus()); -// assertNull(matchEnvelope.getError()); -// assertEquals(200, matchEnvelope.getHttpCode()); -// } -// -// @Test -// @SuppressWarnings("unchecked") -// void updateMatch_withTeamsMembersAndMoves_shouldUpdateMatchTeamsAndMoves() throws Exception { -// // Step 1: Create a prerequisite group with players -// var createGroupDto = new GroupCreateDto(); -// createGroupDto.setProfileNames(List.of("player1", "player2", "player3", "player4")); -// createGroupDto.setName("test-update"); -// createGroupDto.setSportPreset("beerpong"); -// -// var groupResponse = testUtils.performPost(port, "/groups", createGroupDto, GroupDto.class); -// ResponseEnvelope groupEnvelope = (ResponseEnvelope) groupResponse.getBody(); -// var group = groupEnvelope.getData(); -// -// var season = group.getActiveSeason(); -// -// var playersResponse = testUtils.performGet(port, "/groups/" + group.getId() + "/seasons/" + season.getId() + "/players", List.class, PlayerDto.class); -// ResponseEnvelope> playersEnvelope = (ResponseEnvelope>) playersResponse.getBody(); -// var players = playersEnvelope.getData(); -// -// // Step 2: Create a rule move -// var createRuleMoveDto = new RuleMoveDto(); -// createRuleMoveDto.setName("UpdateRuleMove"); -// var createRuleMoveDto1 = new RuleMoveDto(); -// createRuleMoveDto1.setName("UpdateRuleMove"); -// createRuleMoveDto1.setFinishingMove(true); -// -// var ruleMoveResponse = testUtils.performPost(port, "/groups/" + group.getId() + "/seasons/" + season.getId() + "/rule-moves", createRuleMoveDto, RuleMoveDto.class); -// var ruleMoveResponse1 = testUtils.performPost(port, "/groups/" + group.getId() + "/seasons/" + season.getId() + "/rule-moves", createRuleMoveDto1, RuleMoveDto.class); -// ResponseEnvelope ruleMoveEnvelope = (ResponseEnvelope) ruleMoveResponse.getBody(); -// ResponseEnvelope ruleMoveEnvelope1 = (ResponseEnvelope) ruleMoveResponse1.getBody(); -// var ruleMove = ruleMoveEnvelope.getData(); -// var ruleMove1 = ruleMoveEnvelope1.getData(); -// -// // Step 3: Create a match with teams and members -// MatchCreateDto matchCreateDto = new MatchCreateDto(); -// -// // Team 1 -// TeamCreateDto team1 = new TeamCreateDto(); -// TeamMemberCreateDto member1 = new TeamMemberCreateDto(); -// member1.setPlayerId(players.get(0).getId()); -// MatchMoveDto move1 = new MatchMoveDto(); -// move1.setMoveId(ruleMove1.getId()); -// move1.setCount(1); -// member1.setMoves(List.of(move1)); -// team1.setTeamMembers(List.of(member1)); -// -// // Team 2 -// TeamCreateDto team2 = new TeamCreateDto(); -// TeamMemberCreateDto member2 = new TeamMemberCreateDto(); -// member2.setPlayerId(players.get(1).getId()); -// MatchMoveDto move2 = new MatchMoveDto(); -// move2.setMoveId(ruleMove.getId()); -// move2.setCount(5); -// member2.setMoves(List.of(move2)); -// team2.setTeamMembers(List.of(member2)); -// -// matchCreateDto.setTeams(List.of(team1, team2)); -// -// // Step 4: Create the match -// var createMatchResponse = testUtils.performPost(port, "/groups/" + group.getId() + "/seasons/" + season.getId() + "/matches", matchCreateDto, MatchDto.class); -// ResponseEnvelope matchEnvelope = (ResponseEnvelope) createMatchResponse.getBody(); -// assert matchEnvelope != null; -// var match = matchEnvelope.getData(); -// -// team1.getTeamMembers().get(0).getMoves().get(0).setCount(1); -// matchCreateDto.setTeams(List.of(team1, team2)); -// -// // Step 5: Update the match -// var updateResponse = testUtils.performPut(port, "/groups/" + group.getId() + "/seasons/" + season.getId() + "/matches/" + match.getId(), matchCreateDto, MatchDto.class); -// ResponseEnvelope updateEnvelope = (ResponseEnvelope) updateResponse.getBody(); -// -// // Assertions -// assertNotNull(updateResponse); -// assertEquals(200, updateResponse.getStatusCode().value()); -// assertNotNull(updateEnvelope); -// assertEquals(ResponseEnvelope.Status.OK, updateEnvelope.getStatus()); -// assertNull(updateEnvelope.getError()); -// assertEquals(200, updateEnvelope.getHttpCode()); -// } - -// @Test -// @SuppressWarnings("unchecked") -// void getAllMatches_shouldReturnAllMatchesForSeason() throws Exception { -// // Step 1: Create a prerequisite group with players -// var createGroupDto = new GroupCreateDto(); -// createGroupDto.setProfileNames(List.of("player1", "player2")); -// createGroupDto.setName("test-get"); - //createGroupDto.setSportPreset("beerpong"); -// -// var groupResponse = testUtils.performPost(port, "/groups", createGroupDto, GroupDto.class); -// ResponseEnvelope groupEnvelope = (ResponseEnvelope) groupResponse.getBody(); -// var group = groupEnvelope.getData(); -// -// var season = group.getActiveSeason(); -// -// // Step 2: Create a match -// MatchCreateDto matchCreateDto = new MatchCreateDto(); -// TeamCreateDto team = new TeamCreateDto(); -// TeamMemberCreateDto member = new TeamMemberCreateDto(); -// member.setPlayerId("player1-id"); -// team.setTeamMembers(List.of(member)); -// matchCreateDto.setTeams(List.of(team)); -// -// testUtils.performPost(port, "/groups/" + group.getId() + "/seasons/" + season.getId() + "/match", matchCreateDto, MatchDto.class); -// -// // Step 3: Get all matches for the season -// var getAllMatchesResponse = testUtils.performGet(port, "/groups/" + group.getId() + "/seasons/" + season.getId() + "/matches", List.class, MatchDto.class); -// ResponseEnvelope> matchesEnvelope = (ResponseEnvelope>) getAllMatchesResponse.getBody(); -// -// // Assertions -// assertNotNull(getAllMatchesResponse); -// assertEquals(200, getAllMatchesResponse.getStatusCode().value()); -// assertNotNull(matchesEnvelope); -// assertEquals(ResponseEnvelope.Status.OK, matchesEnvelope.getStatus()); -// assertNull(matchesEnvelope.getError()); -// assertEquals(200, matchesEnvelope.getHttpCode()); -// assertNotNull(matchesEnvelope.getData()); -// assertEquals(1, matchesEnvelope.getData().size()); -// } -} \ No newline at end of file diff --git a/api/src/test/java/pro/beerpong/api/RequestUtils.java b/api/src/test/java/pro/beerpong/api/RequestUtils.java new file mode 100644 index 00000000..1d2302ef --- /dev/null +++ b/api/src/test/java/pro/beerpong/api/RequestUtils.java @@ -0,0 +1,263 @@ +package pro.beerpong.api; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.api.client.util.Lists; +import lombok.extern.log4j.Log4j2; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import pro.beerpong.api.auth.JwtTokenProvider; +import pro.beerpong.api.model.dto.*; +import pro.beerpong.api.util.InstallationType; +import pro.beerpong.api.util.TokenType; + +import java.util.List; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Log4j2 +@Component +public class RequestUtils { + private static final List GROUP_ACCESS = Lists.newArrayList(); + private static String REFRESH_TOKEN; + private static String AUTH_TOKEN; + private static boolean RESET_FOR_NEXT_REQUEST = false; + + private final TestRestTemplate restTemplate; + private final JwtTokenProvider jwtTokenProvider; + + public RequestUtils(TestRestTemplate restTemplate, JwtTokenProvider jwtTokenProvider) { + this.restTemplate = restTemplate; + this.jwtTokenProvider = jwtTokenProvider; + } + + public ResponseEntity performGet(int port, String path, Class firstClazz, Class... classes) { + return performCall(true, port, path, HttpMethod.GET, null, firstClazz, classes); + } + + public ResponseEntity performPost(int port, String path, Object body, Class firstClazz, Class... classes) { + return performCall(true, port, path, HttpMethod.POST, body, firstClazz, classes); + } + + public ResponseEntity performPut(int port, String path, Object body, Class firstClazz, Class... classes) { + return performCall(true, port, path, HttpMethod.PUT, body, firstClazz, classes); + } + + public ResponseEntity performDelete(int port, String path, Object body, Class firstClazz, Class... classes) { + return performCall(true, port, path, HttpMethod.DELETE, body, firstClazz, classes); + } + + public void resetAuthForNextRequest() { + RESET_FOR_NEXT_REQUEST = true; + } + + public void resetAuth() { + REFRESH_TOKEN = null; + AUTH_TOKEN = null; + GROUP_ACCESS.clear(); + } + + @SuppressWarnings("unchecked") + private String generateAuthToken(int port) { + String tempRefresh = REFRESH_TOKEN; + + if (RESET_FOR_NEXT_REQUEST || REFRESH_TOKEN == null) { + var signupDto = new AuthSignupDto(); + signupDto.setDeviceId("test"); + signupDto.setInstallationType(InstallationType.IOS); + + var signupResponse = this.performCall(false, port, "/auth/signup", HttpMethod.POST, signupDto, AuthTokenDto.class); + var signupEnvelope = (ResponseEnvelope) signupResponse.getBody(); + assert signupEnvelope != null; + var signupTokenDto = signupEnvelope.getData(); + + // Assertions + assertNotNull(signupResponse); + assertEquals(200, signupResponse.getStatusCode().value()); + assertNotNull(signupEnvelope); + assertEquals(ResponseEnvelope.Status.OK, signupEnvelope.getStatus()); + assertNull(signupEnvelope.getError()); + assertEquals(200, signupEnvelope.getHttpCode()); + assertNotNull(signupTokenDto); + assertEquals(TokenType.REFRESH, signupTokenDto.getType()); + assertNotNull(signupTokenDto.getToken()); + + tempRefresh = signupTokenDto.getToken(); + + if (!RESET_FOR_NEXT_REQUEST) { + REFRESH_TOKEN = tempRefresh; + } + + } + + var tempAuth = AUTH_TOKEN; + + if (RESET_FOR_NEXT_REQUEST || AUTH_TOKEN == null) { + var refreshDto = new AuthRefreshDto(); + refreshDto.setRefreshToken(tempRefresh); + + var refreshResponse = this.performCall(false, port, "/auth/refresh", HttpMethod.POST, refreshDto, AuthTokenDto.class); + var refreshEnvelope = (ResponseEnvelope) refreshResponse.getBody(); + assert refreshEnvelope != null; + var refreshTokenDto = refreshEnvelope.getData(); + + // Assertions + assertNotNull(refreshResponse); + assertEquals(200, refreshResponse.getStatusCode().value()); + assertNotNull(refreshEnvelope); + assertEquals(ResponseEnvelope.Status.OK, refreshEnvelope.getStatus()); + assertNull(refreshEnvelope.getError()); + assertEquals(200, refreshEnvelope.getHttpCode()); + assertNotNull(refreshTokenDto); + assertEquals(TokenType.ACCESS, refreshTokenDto.getType()); + assertNotNull(refreshTokenDto.getToken()); + + tempAuth = refreshTokenDto.getToken(); + + if (!RESET_FOR_NEXT_REQUEST) { + AUTH_TOKEN = tempAuth; + } + } + + RESET_FOR_NEXT_REQUEST = false; + + return tempAuth; + } + + public ResponseEntity performCall(boolean withAuth, int port, String path, HttpMethod method, Object body, Class firstClazz, Class... classes) { + HttpHeaders headers = new HttpHeaders(); + + if (withAuth) { + headers.setBearerAuth(generateAuthToken(port)); + } + + headers.setContentType(MediaType.APPLICATION_JSON); + + var entity = (body == null ? new HttpEntity<>(headers) : new HttpEntity<>(body, headers)); + var exchange = restTemplate.exchange("http://localhost:" + port + path, method, entity, String.class); + + var objectMapper = new ObjectMapper(); + objectMapper.findAndRegisterModules(); + + var typeFactory = objectMapper.getTypeFactory(); + + JavaType valueType = null; + + int limit = classes.length + 1; + + for (int i = limit - 1; i >= 0; i--) { + + if (limit - 1 == 0) { + //list is empty + valueType = typeFactory.constructParametricType(ResponseEnvelope.class, firstClazz); + } else { + //list isn't empty + + if (i == 0) { + //firstClass height + + if (limit - 1 == 1) { + //list has just 1 element + valueType = typeFactory.constructParametricType(firstClazz, classes[i]); + } else { + //list has > 1 element + valueType = typeFactory.constructParametricType(firstClazz, valueType); + } + + valueType = typeFactory.constructParametricType(ResponseEnvelope.class, valueType); + } else { + //over firstClass height, inside classes list + + if (limit - 1 > 1 && i < limit - 1) { + //list has > 1 element + skip last pair + + if (valueType == null) { + valueType = typeFactory.constructParametricType(classes[i-1], classes[i]); + } else { + valueType = typeFactory.constructParametricType(classes[i-1], valueType); + } + } + } + } + } + + var responseBody = exchange.getBody(); + + if (responseBody == null) { + return ResponseEntity.status(exchange.getStatusCode()).build(); + } + + if (!responseBody.startsWith("{") && !responseBody.startsWith("[")) { + return ResponseEntity.status(exchange.getStatusCode()).body(responseBody); + } + + Object responseEnvelope; + + try { + responseEnvelope = objectMapper.readValue(responseBody, valueType); + } catch (JsonProcessingException e) { + log.error("Error whilst parsing body '{}'!", responseBody); + throw new RuntimeException(e); + } + + return ResponseEntity.status(exchange.getStatusCode()).body(responseEnvelope); + } + + @SuppressWarnings("unchecked") + public T assertSuccess(ResponseEntity response, Class tClass) { + assertNotNull(response); + assertEquals(200, response.getStatusCode().value()); + + ResponseEnvelope envelope = (ResponseEnvelope) response.getBody(); + assertNotNull(envelope); + assertEquals(ResponseEnvelope.Status.OK, envelope.getStatus()); + assertEquals(200, envelope.getHttpCode()); + assertNull(envelope.getError()); + assertNotNull(envelope.getData()); + assertEquals(tClass, envelope.getData().getClass()); + + return tClass.cast(envelope.getData()); + } + + @SuppressWarnings("unchecked") + public void assertFailure(ResponseEntity response, ErrorCodes error) { + assertNotNull(response); + assertEquals(error.getHttpStatus().value(), response.getStatusCode().value()); + + ResponseEnvelope envelope = (ResponseEnvelope) response.getBody(); + assertNotNull(envelope); + assertEquals(ResponseEnvelope.Status.ERROR, envelope.getStatus()); + assertEquals(error.getHttpStatus().value(), envelope.getHttpCode()); + assertNotNull(envelope.getError()); + assertEquals(error.getCode(), envelope.getError().getCode()); + assertEquals(error.getDescription(), envelope.getError().getDescription()); + assertNull(envelope.getData()); + } + + public void assertFailure(ResponseEntity response, HttpStatus status, String message) { + assertNotNull(response); + assertEquals(status.value(), response.getStatusCode().value()); + + String result = (String) response.getBody(); + assertNotNull(result); + assertEquals(message, result); + } + + public String currentUserId() { + if (REFRESH_TOKEN == null || REFRESH_TOKEN.trim().isEmpty()) { + return null; + } + + var claims = jwtTokenProvider.validateToken(REFRESH_TOKEN, "refresh"); + + if (claims == null) { + return null; + } + + return claims.getSubject(); + } +} \ No newline at end of file diff --git a/api/src/test/java/pro/beerpong/api/TestUtils.java b/api/src/test/java/pro/beerpong/api/TestUtils.java index ab93bb55..aa6a3538 100644 --- a/api/src/test/java/pro/beerpong/api/TestUtils.java +++ b/api/src/test/java/pro/beerpong/api/TestUtils.java @@ -1,99 +1,173 @@ package pro.beerpong.api; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; -import pro.beerpong.api.model.dto.ResponseEnvelope; +import pro.beerpong.api.model.dto.*; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; @Component -@RequiredArgsConstructor public class TestUtils { - private final TestRestTemplate restTemplate; + //TODO tests for: realtime events, assets (needs s3 files), leaderboard (+stats) + //TODO add descr to every test case + + @Autowired + private RequestUtils requestUtils; + + /* GROUPS */ + public void assertGroupEquals(GroupDto expected, GroupDto actual) { + if (expected == null || actual == null) { + assertNull(expected); + assertNull(actual); + return; + } - public ResponseEntity performGet(int port, String path, Class firstClazz, Class... classes) { - return performCall(port, path, HttpMethod.GET, null, firstClazz, classes); + actual.setCreatedAt(expected.getCreatedAt()); + actual.getActiveSeason().setStartDate(expected.getActiveSeason().getStartDate()); + + assertEquals(expected, actual); } - public ResponseEntity performPost(int port, String path, Object body, Class firstClazz, Class... classes) { - return performCall(port, path, HttpMethod.POST, body, firstClazz, classes); + public GroupDto createTestGroup(int port) { + return this.createTestGroup(port, "test"); } - public ResponseEntity performPut(int port, String path, Object body, Class firstClazz, Class... classes) { - return performCall(port, path, HttpMethod.PUT, body, firstClazz, classes); + public GroupDto createTestGroup(int port, String name) { + return this.createTestGroup(port, name, List.of("player1", "player2")); } - public ResponseEntity performDelete(int port, String path, Object body, Class firstClazz, Class... classes) { - return performCall(port, path, HttpMethod.DELETE, body, firstClazz, classes); + public GroupDto createTestGroup(int port, List profileNames) { + return this.createTestGroup(port, "test", profileNames); } - public ResponseEntity performCall(int port, String path, HttpMethod method, Object body, Class firstClazz, Class... classes) { - var exchange = restTemplate.exchange("http://localhost:" + port + path, method, body == null ? null : new HttpEntity<>(body), String.class); + public GroupDto createTestGroup(int port, String name, List profileNames) { + return this.createTestGroup(port, name, profileNames, "beerpong", null); + } - var objectMapper = new ObjectMapper(); - objectMapper.findAndRegisterModules(); + public GroupDto createTestGroup(int port, String name, String preset) { + return createTestGroup(port, name, preset, null); + } - var typeFactory = objectMapper.getTypeFactory(); + public GroupDto createTestGroup(int port, String name, String preset, String customSportName) { + return createTestGroup(port, name, List.of("player1", "player2"), preset, customSportName); + } - JavaType valueType = null; + public GroupDto createTestGroup(int port, String name, List profileNames, String preset, String customSportName) { + var response = postGroup(port, name, profileNames, preset, customSportName); + return requestUtils.assertSuccess(response, GroupDto.class); + } - int limit = classes.length + 1; + public ResponseEntity postGroup(int port, String name, List profileNames, String preset) { + return this.postGroup(port, name, profileNames, preset, null); + } - for (int i = limit - 1; i >= 0; i--) { + public ResponseEntity postGroup(int port, String name, List profileNames, String preset, String customSportName) { + var createDto = new GroupCreateDto(); + createDto.setProfileNames(profileNames); + createDto.setName(name); + createDto.setSportPreset(preset); + createDto.setCustomSportName(customSportName); - if (limit - 1 == 0) { - //list is empty - valueType = typeFactory.constructParametricType(ResponseEnvelope.class, firstClazz); - } else { - //list isn't empty + return requestUtils.performPost(port, "/groups", createDto, GroupDto.class); + } - if (i == 0) { - //firstClass height + /* RULE MOVES */ + public RuleMoveCreateDto buildRuleMove(String name, boolean finish, int scorerPoints, int teamPoints) { + var ruleMove = new RuleMoveCreateDto(); + ruleMove.setName(name); + ruleMove.setFinishingMove(finish); + ruleMove.setPointsForScorer(scorerPoints); + ruleMove.setPointsForTeam(teamPoints); + return ruleMove; + } - if (limit - 1 == 1) { - //list has just 1 element - valueType = typeFactory.constructParametricType(firstClazz, classes[i]); - } else { - //list has > 1 element - valueType = typeFactory.constructParametricType(firstClazz, valueType); - } + public void assertRuleMovesEquals(List expected, List actual) { + assertRuleMovesEquals(expected, actual, false); + } - valueType = typeFactory.constructParametricType(ResponseEnvelope.class, valueType); - } else { - //over firstClass height, inside classes list + public void assertRuleMovesEquals(List expected, List actual, boolean full) { + assertEquals(expected.size(), actual.size()); - if (limit - 1 > 1 && i < limit - 1) { - //list has > 1 element + skip last pair + for (int i = 0; i < expected.size(); i++) { + assertRuleMoveEquals(expected.get(i), actual.get(i), full); + } + } - if (valueType == null) { - valueType = typeFactory.constructParametricType(classes[i-1], classes[i]); - } else { - valueType = typeFactory.constructParametricType(classes[i-1], valueType); - } - } - } - } + public void assertRuleMoveEquals(RuleMoveDto expected, RuleMoveDto actual, boolean full) { + if (full) { + assertEquals(expected.getId(), actual.getId()); + assertEquals(expected.getSeason().getId(), actual.getSeason().getId()); } + assertEquals(expected.getName(), actual.getName()); + assertEquals(expected.isFinishingMove(), actual.isFinishingMove()); + assertEquals(expected.getPointsForScorer(), actual.getPointsForScorer()); + assertEquals(expected.getPointsForTeam(), actual.getPointsForTeam()); + } - var responseBody = exchange.getBody(); + public void assertCreatedRuleMovesEquals(List created, List actual) { + assertEquals(created.size(), actual.size()); - if (responseBody == null) { - return ResponseEntity.status(exchange.getStatusCode()).build(); + for (int i = 0; i < created.size(); i++) { + assertCreatedRuleMoveEquals(created.get(i), actual.get(i)); } + } - Object responseEnvelope; + public void assertCreatedRuleMoveEquals(RuleMoveCreateDto createDto, RuleMoveDto actual) { + assertEquals(createDto.getName(), actual.getName()); + assertEquals(createDto.isFinishingMove(), actual.isFinishingMove()); + assertEquals(createDto.getPointsForScorer(), actual.getPointsForScorer()); + assertEquals(createDto.getPointsForTeam(), actual.getPointsForTeam()); + } - try { - responseEnvelope = objectMapper.readValue(responseBody, valueType); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); + /* RULES */ + public void assertRulesEquals(List expected, List actual) { + assertRulesEquals(expected, actual, false); + } + + public void assertRulesEquals(List expected, List actual, boolean full) { + assertEquals(expected.size(), actual.size()); + + for (int i = 0; i < expected.size(); i++) { + assertRuleEquals(expected.get(i), actual.get(i), full); + } + } + + public void assertRuleEquals(RuleDto expected, RuleDto actual, boolean full) { + if (full) { + assertEquals(expected.getId(), actual.getId()); + assertEquals(expected.getSeason().getId(), actual.getSeason().getId()); + assertEquals(expected.getCreatedBy().getUserId(), actual.getCreatedBy().getGroupId()); + } + assertEquals(expected.getTitle(), actual.getTitle()); + assertEquals(expected.getDescription(), actual.getDescription()); + } + + public void assertCreatedRulesEquals(List created, List actual) { + assertEquals(created.size(), actual.size()); + + for (int i = 0; i < created.size(); i++) { + assertCreatedRuleEquals(created.get(i), actual.get(i)); + } + } + + public void assertCreatedRuleEquals(RuleCreateDto createDto, RuleDto actual) { + assertEquals(createDto.getDescription(), actual.getDescription()); + assertEquals(createDto.getTitle(), actual.getTitle()); + } + + /* SEASONS */ + public void assertSeasonEquals(SeasonDto expected, SeasonDto actual) { + if (expected == null || actual == null) { + assertNull(expected); + assertNull(actual); + return; } - return ResponseEntity.status(exchange.getStatusCode()).body(responseEnvelope); + actual.setStartDate(expected.getStartDate()); + + assertEquals(expected, actual); } -} \ No newline at end of file +} diff --git a/api/src/test/java/pro/beerpong/api/control/AssetControllerTest.java b/api/src/test/java/pro/beerpong/api/control/AssetControllerTest.java index 22813f8b..de2a15f7 100644 --- a/api/src/test/java/pro/beerpong/api/control/AssetControllerTest.java +++ b/api/src/test/java/pro/beerpong/api/control/AssetControllerTest.java @@ -1,27 +1,14 @@ package pro.beerpong.api.control; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.context.ActiveProfiles; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.support.DefaultTransactionDefinition; -import pro.beerpong.api.TestUtils; -import pro.beerpong.api.model.dto.AssetMetadataDto; -import pro.beerpong.api.model.dto.ResponseEnvelope; - -import java.time.Duration; -import java.time.ZonedDateTime; - -import static org.junit.jupiter.api.Assertions.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("test") public class AssetControllerTest { - /*@LocalServerPort + /* + TODO needs s3 files + @LocalServerPort private int port; @Autowired @@ -40,26 +27,16 @@ public void createTransaction() { @Test @SuppressWarnings("unchecked") - public void whenUploadingAsset_ThenIsSuccessful() { - var response = testUtils.performPost(port, "/assets", new byte[] {-128, 0, 127, 0}, AssetMetadataDto.class); - - assertNotNull(response); - assertEquals(200, response.getStatusCode().value()); - - var envelope = (ResponseEnvelope) response.getBody(); - assertNotNull(envelope); - assertEquals(ResponseEnvelope.Status.OK, envelope.getStatus()); - assertNull(envelope.getError()); - assertEquals(200, envelope.getHttpCode()); + public void assets_groupWallpaper_success() { + var createDto = new GroupCreateDto(); + createDto.setProfileNames(List.of("player1", "player2")); + createDto.setName("test"); + createDto.setSportPreset("beerpong"); - var assetMetadata = envelope.getData(); + var groupResponse = testUtils.performPost(port, "/groups", createDto, GroupDto.class); + var group = testUtils.assertSuccess(groupResponse, GroupDto.class); - assertNotNull(assetMetadata); - assertNotNull(assetMetadata.getId()); - assertNotNull(assetMetadata.getMediaType()); - assertEquals("application/octet-stream", assetMetadata.getMediaType()); - assertNotNull(assetMetadata.getUploadedAt()); - assertTrue(60 > Duration.between(ZonedDateTime.now(),assetMetadata.getUploadedAt()).getSeconds()); + var response = testUtils.performPut(port, "/groups/" + group.getId(), group, AssetMetadataDto.class); } @Test diff --git a/api/src/test/java/pro/beerpong/api/control/GroupControllerTest.java b/api/src/test/java/pro/beerpong/api/control/GroupControllerTest.java new file mode 100644 index 00000000..8208efe3 --- /dev/null +++ b/api/src/test/java/pro/beerpong/api/control/GroupControllerTest.java @@ -0,0 +1,280 @@ +package pro.beerpong.api.control; + +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import pro.beerpong.api.TestUtils; +import pro.beerpong.api.RequestUtils; +import pro.beerpong.api.model.dto.ErrorCodes; +import pro.beerpong.api.model.dto.GroupCreateDto; +import pro.beerpong.api.model.dto.GroupDto; +import pro.beerpong.api.util.DailyLeaderboard; +import pro.beerpong.api.util.RankingAlgorithm; + +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +public class GroupControllerTest { + @LocalServerPort + private int port; + + @Autowired + private RequestUtils requestUtils; + @Autowired + private TestUtils testUtils; + + @Test + @Transactional + public void group_create_success() { + var name = "test"; + var group = testUtils.createTestGroup(port, name); + + // test simple group creation + assertNotNull(group); + assertNotNull(group.getId()); + assertNotNull(group.getName()); + assertEquals(name, group.getName()); + assertNotNull(group.getInviteCode()); + assertNotNull(group.getCreatedAt()); + assertNull(group.getWallpaperAsset()); + assertNull(group.getCustomSportName()); + assertEquals(GroupPresetsController.BEERPONG.getId(), group.getSportPreset().getId()); + + assertNotNull(group.getActiveSeason()); + assertNotNull(group.getActiveSeason().getId()); + assertNull(group.getActiveSeason().getName()); + assertNotNull(group.getActiveSeason().getStartDate()); + assertNull(group.getActiveSeason().getEndDate()); + assertEquals(group.getActiveSeason().getGroupId(), group.getId()); + assertNotNull(group.getActiveSeason().getSeasonSettings()); + assertEquals(1, group.getActiveSeason().getSeasonSettings().getMinMatchesToQualify()); + assertEquals(1, group.getActiveSeason().getSeasonSettings().getMinTeamSize()); + assertEquals(10, group.getActiveSeason().getSeasonSettings().getMaxTeamSize()); + assertEquals(RankingAlgorithm.AVERAGE, group.getActiveSeason().getSeasonSettings().getRankingAlgorithm()); + assertEquals(DailyLeaderboard.WAKE_TIME, group.getActiveSeason().getSeasonSettings().getDailyLeaderboard()); + assertEquals(LocalTime.of(0, 0), group.getActiveSeason().getSeasonSettings().getWakeTime()); + assertEquals(requestUtils.currentUserId(), group.getCreatedBy().getUserId()); + assertEquals(group.getId(), group.getCreatedBy().getGroupId()); + assertEquals(group.getCreatedBy(), group.getActiveSeason().getCreatedBy()); + + group = testUtils.createTestGroup(port, "test", List.of("player1", "player2"), null, "test123"); + + // test group creation with custom sport name + assertNotNull(group); + assertEquals("test123", group.getCustomSportName()); + assertNull(group.getSportPreset()); + + group = testUtils.createTestGroup(port, "test", List.of("player1", "player2"), "kicker", "test123"); + + // test group creation with other game preset + assertNotNull(group); + assertNull(group.getCustomSportName()); + assertEquals(GroupPresetsController.KICKER.getId(), group.getSportPreset().getId()); + } + + @Test + @Transactional + public void group_create_invalidName() { + // test invalid group name (empty) + var response = testUtils.postGroup(port, "", List.of("player1", "player2"), "beerpong"); + requestUtils.assertFailure(response, ErrorCodes.INVALID_GROUP_NAME); + + // test invalid group name (null) + response = testUtils.postGroup(port, null, List.of("player1", "player2"), "beerpong"); + requestUtils.assertFailure(response, ErrorCodes.INVALID_GROUP_NAME); + + // test invalid group name (too short) + response = testUtils.postGroup(port, "a", List.of("player1", "player2"), "beerpong"); + requestUtils.assertFailure(response, ErrorCodes.INVALID_GROUP_NAME); + + // test invalid group name (too long) + response = testUtils.postGroup(port, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", List.of("player1", "player2"), "beerpong"); + requestUtils.assertFailure(response, ErrorCodes.INVALID_GROUP_NAME); + } + + @Test + @Transactional + public void group_create_invalidProfiles() { + // test invalid profile names (empty) + var response = testUtils.postGroup(port, "test", List.of(), "beerpong"); + requestUtils.assertFailure(response, ErrorCodes.INVALID_GROUP_PROFILE_NAMES); + + // test invalid profile names (null) + response = testUtils.postGroup(port, "test", null, "beerpong"); + requestUtils.assertFailure(response, ErrorCodes.INVALID_GROUP_PROFILE_NAMES); + + // test invalid profile names (non unique names) + response = testUtils.postGroup(port, "test", List.of("player1", "player2", "player2"), "beerpong"); + requestUtils.assertFailure(response, ErrorCodes.INVALID_GROUP_PROFILE_NAMES); + } + + @Test + @Transactional + public void group_create_invalidSport() { + // test invalid sport (non existing preset) + var response = testUtils.postGroup(port, "test", List.of("player1", "player2"), "notExisting"); + requestUtils.assertFailure(response, ErrorCodes.INVALID_GROUP_SPORT); + + // test invalid sport (preset null) + response = testUtils.postGroup(port, "test", List.of("player1", "player2"), null); + requestUtils.assertFailure(response, ErrorCodes.INVALID_GROUP_SPORT); + + // test invalid sport (custom sport empty) + response = testUtils.postGroup(port, "test", List.of("player1", "player2"), null, " "); + requestUtils.assertFailure(response, ErrorCodes.INVALID_GROUP_SPORT); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void group_userGroups() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var response = requestUtils.performGet(port, "/groups/user", List.class, GroupDto.class); + var groups = (List) requestUtils.assertSuccess(response, ArrayList.class); + + // test groups a user has access to + assertFalse(groups.isEmpty()); + assertTrue(groups.stream().anyMatch(groupDto -> groupDto.getId().equals(prerequisiteGroup.getId()))); + } + + @Test + @Transactional + public void group_findByInviteCode_success() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var response = requestUtils.performGet(port, "/groups?inviteCode=" + prerequisiteGroup.getInviteCode(), GroupDto.class); + var group = requestUtils.assertSuccess(response, GroupDto.class); + + // test group by invite code + assertNotNull(group); + testUtils.assertGroupEquals(prerequisiteGroup, group); + } + + @Test + @Transactional + public void group_findByInviteCode_invalidInviteCode() { + // test invalid inviteCode (empty) + var response = requestUtils.performGet(port, "/groups?inviteCode= ", GroupDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_GROUP_INVITE_CODE); + + // test invalid inviteCode (not existing) + response = requestUtils.performGet(port, "/groups?inviteCode=someIdThatNotExists", GroupDto.class); + requestUtils.assertFailure(response, ErrorCodes.GROUP_INVITE_NOT_FOUND); + } + + @Test + @Transactional + public void group_findById_success() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId(), GroupDto.class); + var group = requestUtils.assertSuccess(response, GroupDto.class); + + // test group by id + assertNotNull(group); + testUtils.assertGroupEquals(prerequisiteGroup, group); + } + + @Test + public void group_findById_invalidId() { + // test invalid group id (not existing) + var response = requestUtils.performGet(port, "/groups/someIdThatNotExists", GroupDto.class); + requestUtils.assertFailure(response, HttpStatus.UNAUTHORIZED, "No access to this group!"); + } + + @Test + @Transactional + public void group_update_success() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var createDto = new GroupCreateDto(); + createDto.setName("test123"); + createDto.setCustomSportName("kicker"); + createDto.setProfileNames(List.of("player3", "player4", "player1")); + + var response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId(), createDto, GroupDto.class); + var group = requestUtils.assertSuccess(response, GroupDto.class); + + // test group update + assertNotNull(prerequisiteGroup); + assertNotNull(group); + assertEquals(prerequisiteGroup.getId(), group.getId()); + assertEquals(createDto.getName(), group.getName()); + assertEquals(prerequisiteGroup.getInviteCode(), group.getInviteCode()); + assertEquals(prerequisiteGroup.getCreatedBy(), group.getCreatedBy()); + assertEquals(prerequisiteGroup.getWallpaperAsset(), group.getWallpaperAsset()); + assertEquals(prerequisiteGroup.getCustomSportName(), group.getCustomSportName()); + assertEquals(prerequisiteGroup.getSportPreset(), group.getSportPreset()); + testUtils.assertSeasonEquals(prerequisiteGroup.getActiveSeason(), group.getActiveSeason()); + } + + @Test + public void group_update_invalidName() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var createDto = new GroupCreateDto(); + createDto.setName(" "); + + // test invalid group name (empty) + var response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId(), createDto, GroupDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_GROUP_NAME); + + createDto.setName(null); + + // test invalid group name (null) + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId(), createDto, GroupDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_GROUP_NAME); + + createDto.setName("a"); + + // test invalid group name (too short) + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId(), createDto, GroupDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_GROUP_NAME); + + createDto.setName("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + + // test invalid group name (too long) + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId(), createDto, GroupDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_GROUP_NAME); + } + + @Test + @Transactional + public void group_join_success() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + // test group join (already in group) + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/join", null, String.class); + requestUtils.assertFailure(response, ErrorCodes.GROUP_ALREADY_IN_GROUP); + + requestUtils.resetAuthForNextRequest(); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/join", null, String.class); + var ok = requestUtils.assertSuccess(response, String.class); + + // test group join (not in group) + assertEquals("OK", ok); + } + + @Test + @Transactional + public void group_leave() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/leave", null, String.class); + var ok = requestUtils.assertSuccess(response, String.class); + + // test group leave + assertEquals("OK", ok); + } +} \ No newline at end of file diff --git a/api/src/test/java/pro/beerpong/api/control/LeaderboardControllerTest.java b/api/src/test/java/pro/beerpong/api/control/LeaderboardControllerTest.java new file mode 100644 index 00000000..b5ee3989 --- /dev/null +++ b/api/src/test/java/pro/beerpong/api/control/LeaderboardControllerTest.java @@ -0,0 +1,72 @@ +package pro.beerpong.api.control; + +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import pro.beerpong.api.RequestUtils; +import pro.beerpong.api.TestUtils; +import pro.beerpong.api.model.dto.ErrorCodes; +import pro.beerpong.api.model.dto.GroupCreateDto; +import pro.beerpong.api.model.dto.GroupDto; +import pro.beerpong.api.model.dto.LeaderboardDto; +import pro.beerpong.api.util.DailyLeaderboard; +import pro.beerpong.api.util.RankingAlgorithm; + +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +public class LeaderboardControllerTest { + @LocalServerPort + private int port; + + @Autowired + private RequestUtils requestUtils; + @Autowired + private TestUtils testUtils; + + @Test + @Transactional + public void leaderboard_get_Alltime_empty() { + var profileNames = List.of("player1", "player2", "player3"); + var prerequisiteGroup = testUtils.createTestGroup(port, "test", profileNames); + + var response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/leaderboard?scope=all-time", LeaderboardDto.class); + var leaderboard = requestUtils.assertSuccess(response, LeaderboardDto.class); + + assertEquals(0, leaderboard.getNumMatches()); + assertEquals(3, leaderboard.getNumPlayers()); + assertEquals(prerequisiteGroup.getCreatedAt().toEpochSecond(), leaderboard.getStartedAt().toEpochSecond()); + } + + @Test + public void leaderboard_get_invalidScope() { + var prerequisiteGroup = testUtils.createTestGroup(port, "test"); + + // test invalid scope (non provided) + var response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/leaderboard?scope=something", LeaderboardDto.class); + requestUtils.assertFailure(response, ErrorCodes.LEADERBOARD_SCOPE_NOT_FOUND); + + // test invalid scope (scope=season without season id) + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/leaderboard?scope=season", LeaderboardDto.class); + requestUtils.assertFailure(response, ErrorCodes.LEADERBOARD_SEASON_NOT_FOUND); + + // test invalid scope (invalid season id) + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/leaderboard?scope=season&seasonId=someId", LeaderboardDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_FOUND); + + var prerequisiteGroup1 = testUtils.createTestGroup(port, "test123"); + + // test invalid scope (season id from other group) + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/leaderboard?scope=season&seasonId=" + prerequisiteGroup1.getActiveSeason().getId(), LeaderboardDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_OF_GROUP); + } +} \ No newline at end of file diff --git a/api/src/test/java/pro/beerpong/api/control/MatchControllerTest.java b/api/src/test/java/pro/beerpong/api/control/MatchControllerTest.java new file mode 100644 index 00000000..4d5a7bfa --- /dev/null +++ b/api/src/test/java/pro/beerpong/api/control/MatchControllerTest.java @@ -0,0 +1,3371 @@ +package pro.beerpong.api.control; + +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; +import pro.beerpong.api.RequestUtils; +import pro.beerpong.api.TestUtils; +import pro.beerpong.api.model.dto.*; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +public class MatchControllerTest { + @LocalServerPort + private int port; + + @Autowired + private RequestUtils requestUtils; + @Autowired + private TestUtils testUtils; + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void matches_create_success_basic() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertTrue(players.size() >= 2); + + var player1 = players.getFirst(); + var player2 = players.getLast(); + + assertNotNull(player1); + assertNotNull(player2); + assertNotEquals(player1, player2); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMove = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(normalMove); + assertFalse(normalMove.isFinishingMove()); + assertNotNull(finishMove); + assertTrue(finishMove.isFinishingMove()); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 5), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId(), + buildMove(normalMove.getId(), 3) + ) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var match = requestUtils.assertSuccess(response, MatchDto.class); + + assertNotNull(match.getId()); + assertEquals(prerequisiteGroup.getActiveSeason().getId(), match.getSeason().getId()); + assertEquals(prerequisiteGroup.getCreatedBy(), match.getCreatedBy()); + assertNotNull(match.getDate()); + + assertEquals(2, match.getTeams().size()); + + var team1 = match.getTeams().getFirst(); + var team2 = match.getTeams().getLast(); + + assertNotNull(team1); + assertNotNull(team1.getId()); + assertEquals(match.getId(), team1.getMatchId()); + assertNotNull(team2); + assertNotNull(team2.getId()); + assertEquals(match.getId(), team2.getMatchId()); + + var teamMembers1 = match.getTeamMembers().stream() + .filter(teamMemberDto -> teamMemberDto.getTeamId().equals(team1.getId())) + .toList(); + var teamMembers2 = match.getTeamMembers().stream() + .filter(teamMemberDto -> teamMemberDto.getTeamId().equals(team2.getId())) + .toList(); + + assertEquals(1, teamMembers1.size()); + assertEquals(1, teamMembers2.size()); + + var teamMember1 = teamMembers1.getFirst(); + var teamMember2 = teamMembers2.getFirst(); + + assertNotNull(teamMember1); + assertNotNull(teamMember1.getId()); + assertEquals(team1.getId(), teamMember1.getTeamId()); + assertEquals(player1.getId(), teamMember1.getPlayerId()); + assertNotNull(teamMember2); + assertNotNull(teamMember2.getId()); + assertEquals(team2.getId(), teamMember2.getTeamId()); + assertEquals(player2.getId(), teamMember2.getPlayerId()); + + var matchMoves1 = match.getMatchMoves().stream() + .filter(matchMove -> matchMove.getTeamMemberId().equals(teamMember1.getId())) + .toList(); + var matchMoves2 = match.getMatchMoves().stream() + .filter(matchMove -> matchMove.getTeamMemberId().equals(teamMember2.getId())) + .toList(); + + assertEquals(2, matchMoves1.size()); + assertEquals(1, matchMoves2.size()); + + var normalMove1 = matchMoves1.stream().filter(dto -> dto.getMoveId().equals(normalMove.getId())).findFirst().orElseThrow(); + var finishMove1 = matchMoves1.stream().filter(dto -> dto.getMoveId().equals(finishMove.getId())).findFirst().orElseThrow(); + var normalMove2 = matchMoves2.getFirst(); + + assertNotNull(normalMove1); + assertNotNull(normalMove1.getId()); + assertEquals(teamMember1.getId(), normalMove1.getTeamMemberId()); + assertEquals(normalMove.getId(), normalMove1.getMoveId()); + assertEquals(5, normalMove1.getValue()); + + assertNotNull(finishMove1); + assertNotNull(finishMove1.getId()); + assertEquals(teamMember1.getId(), finishMove1.getTeamMemberId()); + assertEquals(finishMove.getId(), finishMove1.getMoveId()); + assertEquals(1, finishMove1.getValue()); + + assertNotNull(normalMove2); + assertNotNull(normalMove2.getId()); + assertEquals(teamMember2.getId(), normalMove2.getTeamMemberId()); + assertEquals(normalMove.getId(), normalMove2.getMoveId()); + assertEquals(3, normalMove2.getValue()); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void matches_create_success_morePlayers() { + var profileNames = List.of("player1", "player2", "player3", "player4"); + var prerequisiteGroup = testUtils.createTestGroup(port, profileNames); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertEquals(profileNames.size(), players.size()); + + var player1 = players.getFirst(); + var player2 = players.get(1); + var player3 = players.get(2); + var player4 = players.get(3); + + assertNotNull(player1); + assertNotNull(player2); + assertNotNull(player3); + assertNotNull(player4); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMove = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(normalMove); + assertFalse(normalMove.isFinishingMove()); + assertNotNull(finishMove); + assertTrue(finishMove.isFinishingMove()); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 5), + buildMove(finishMove.getId(), 1) + ), + buildMember( + player2.getId(), + buildMove(normalMove.getId(), 2) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 4) + ), + buildMember( + player4.getId(), + buildMove(normalMove.getId(), 2) + ) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var match = requestUtils.assertSuccess(response, MatchDto.class); + + assertNotNull(match.getId()); + assertEquals(prerequisiteGroup.getActiveSeason().getId(), match.getSeason().getId()); + assertEquals(prerequisiteGroup.getCreatedBy(), match.getCreatedBy()); + assertNotNull(match.getDate()); + + assertEquals(2, match.getTeams().size()); + + var team1 = match.getTeams().getFirst(); + var team2 = match.getTeams().getLast(); + + assertNotNull(team1); + assertNotNull(team1.getId()); + assertEquals(match.getId(), team1.getMatchId()); + assertNotNull(team2); + assertNotNull(team2.getId()); + assertEquals(match.getId(), team2.getMatchId()); + + var teamMembers1 = match.getTeamMembers().stream() + .filter(teamMemberDto -> teamMemberDto.getTeamId().equals(team1.getId())) + .toList(); + var teamMembers2 = match.getTeamMembers().stream() + .filter(teamMemberDto -> teamMemberDto.getTeamId().equals(team2.getId())) + .toList(); + + assertEquals(2, teamMembers1.size()); + assertEquals(2, teamMembers2.size()); + + var teamMember1 = teamMembers1.getFirst(); + var teamMember2 = teamMembers1.getLast(); + var teamMember3 = teamMembers2.getFirst(); + var teamMember4 = teamMembers2.getLast(); + + assertNotNull(teamMember1); + assertNotNull(teamMember1.getId()); + assertEquals(team1.getId(), teamMember1.getTeamId()); + assertEquals(player1.getId(), teamMember1.getPlayerId()); + assertNotNull(teamMember2); + assertNotNull(teamMember2.getId()); + assertEquals(team1.getId(), teamMember2.getTeamId()); + assertEquals(player2.getId(), teamMember2.getPlayerId()); + assertNotNull(teamMember3); + assertNotNull(teamMember3.getId()); + assertEquals(team2.getId(), teamMember3.getTeamId()); + assertEquals(player3.getId(), teamMember3.getPlayerId()); + assertNotNull(teamMember4); + assertNotNull(teamMember4.getId()); + assertEquals(team2.getId(), teamMember4.getTeamId()); + assertEquals(player4.getId(), teamMember4.getPlayerId()); + + var matchMoves1 = match.getMatchMoves().stream() + .filter(matchMove -> matchMove.getTeamMemberId().equals(teamMember1.getId())) + .toList(); + var matchMoves2 = match.getMatchMoves().stream() + .filter(matchMove -> matchMove.getTeamMemberId().equals(teamMember2.getId())) + .toList(); + var matchMoves3 = match.getMatchMoves().stream() + .filter(matchMove -> matchMove.getTeamMemberId().equals(teamMember3.getId())) + .toList(); + var matchMoves4 = match.getMatchMoves().stream() + .filter(matchMove -> matchMove.getTeamMemberId().equals(teamMember4.getId())) + .toList(); + + assertEquals(2, matchMoves1.size()); + assertEquals(1, matchMoves2.size()); + assertEquals(1, matchMoves3.size()); + assertEquals(1, matchMoves4.size()); + + var normalMove1 = matchMoves1.stream().filter(dto -> dto.getMoveId().equals(normalMove.getId())).findFirst().orElseThrow(); + var finishMove1 = matchMoves1.stream().filter(dto -> dto.getMoveId().equals(finishMove.getId())).findFirst().orElseThrow(); + var normalMove2 = matchMoves2.getFirst(); + var normalMove3 = matchMoves3.getFirst(); + var normalMove4 = matchMoves4.getFirst(); + + assertNotNull(normalMove1); + assertNotNull(normalMove1.getId()); + assertEquals(teamMember1.getId(), normalMove1.getTeamMemberId()); + assertEquals(normalMove.getId(), normalMove1.getMoveId()); + assertEquals(5, normalMove1.getValue()); + + assertNotNull(finishMove1); + assertNotNull(finishMove1.getId()); + assertEquals(teamMember1.getId(), finishMove1.getTeamMemberId()); + assertEquals(finishMove.getId(), finishMove1.getMoveId()); + assertEquals(1, finishMove1.getValue()); + + assertNotNull(normalMove2); + assertNotNull(normalMove2.getId()); + assertEquals(teamMember2.getId(), normalMove2.getTeamMemberId()); + assertEquals(normalMove.getId(), normalMove2.getMoveId()); + assertEquals(2, normalMove2.getValue()); + + assertNotNull(normalMove3); + assertNotNull(normalMove3.getId()); + assertEquals(teamMember3.getId(), normalMove3.getTeamMemberId()); + assertEquals(normalMove.getId(), normalMove3.getMoveId()); + assertEquals(4, normalMove3.getValue()); + + assertNotNull(normalMove4); + assertNotNull(normalMove4.getId()); + assertEquals(teamMember4.getId(), normalMove4.getTeamMemberId()); + assertEquals(normalMove.getId(), normalMove4.getMoveId()); + assertEquals(2, normalMove4.getValue()); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void matches_create_success_moreMoves() { + var profileNames = List.of("player1", "player2", "player3", "player4"); + var prerequisiteGroup = testUtils.createTestGroup(port, profileNames); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertEquals(profileNames.size(), players.size()); + + var player1 = players.getFirst(); + var player2 = players.get(1); + var player3 = players.get(2); + var player4 = players.get(3); + + assertNotNull(player1); + assertNotNull(player2); + assertNotNull(player3); + assertNotNull(player4); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMoves = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).toList(); + + assertTrue(normalMoves.size() >= 2); + + var frstNormalMove = normalMoves.getFirst(); + var scndNormalMove = normalMoves.get(1); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(frstNormalMove); + assertFalse(frstNormalMove.isFinishingMove()); + assertNotNull(scndNormalMove); + assertFalse(scndNormalMove.isFinishingMove()); + assertNotNull(finishMove); + assertTrue(finishMove.isFinishingMove()); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(frstNormalMove.getId(), 5), + buildMove(scndNormalMove.getId(), 2), + buildMove(finishMove.getId(), 1), + buildMove(finishMove.getId(), 0) + ), + buildMember( + player2.getId(), + buildMove(frstNormalMove.getId(), 2), + buildMove(finishMove.getId(), 0) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(frstNormalMove.getId(), 4), + buildMove(scndNormalMove.getId(), 6), + buildMove(finishMove.getId(), 0) + ), + buildMember( + player4.getId(), + buildMove(scndNormalMove.getId(), 2), + buildMove(finishMove.getId(), 0) + ) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var match = requestUtils.assertSuccess(response, MatchDto.class); + + assertNotNull(match.getId()); + assertEquals(prerequisiteGroup.getActiveSeason().getId(), match.getSeason().getId()); + assertEquals(prerequisiteGroup.getCreatedBy(), match.getCreatedBy()); + assertNotNull(match.getDate()); + + assertEquals(2, match.getTeams().size()); + + var team1 = match.getTeams().getFirst(); + var team2 = match.getTeams().getLast(); + + assertNotNull(team1); + assertNotNull(team1.getId()); + assertEquals(match.getId(), team1.getMatchId()); + assertNotNull(team2); + assertNotNull(team2.getId()); + assertEquals(match.getId(), team2.getMatchId()); + + var teamMembers1 = match.getTeamMembers().stream() + .filter(teamMemberDto -> teamMemberDto.getTeamId().equals(team1.getId())) + .toList(); + var teamMembers2 = match.getTeamMembers().stream() + .filter(teamMemberDto -> teamMemberDto.getTeamId().equals(team2.getId())) + .toList(); + + assertEquals(2, teamMembers1.size()); + assertEquals(2, teamMembers2.size()); + + var teamMember1 = teamMembers1.getFirst(); + var teamMember2 = teamMembers1.getLast(); + var teamMember3 = teamMembers2.getFirst(); + var teamMember4 = teamMembers2.getLast(); + + assertNotNull(teamMember1); + assertNotNull(teamMember1.getId()); + assertEquals(team1.getId(), teamMember1.getTeamId()); + assertEquals(player1.getId(), teamMember1.getPlayerId()); + + assertNotNull(teamMember2); + assertNotNull(teamMember2.getId()); + assertEquals(team1.getId(), teamMember2.getTeamId()); + assertEquals(player2.getId(), teamMember2.getPlayerId()); + + assertNotNull(teamMember3); + assertNotNull(teamMember3.getId()); + assertEquals(team2.getId(), teamMember3.getTeamId()); + assertEquals(player3.getId(), teamMember3.getPlayerId()); + + assertNotNull(teamMember4); + assertNotNull(teamMember4.getId()); + assertEquals(team2.getId(), teamMember4.getTeamId()); + assertEquals(player4.getId(), teamMember4.getPlayerId()); + + var matchMoves1 = match.getMatchMoves().stream() + .filter(matchMove -> matchMove.getTeamMemberId().equals(teamMember1.getId())) + .toList(); + var matchMoves2 = match.getMatchMoves().stream() + .filter(matchMove -> matchMove.getTeamMemberId().equals(teamMember2.getId())) + .toList(); + var matchMoves3 = match.getMatchMoves().stream() + .filter(matchMove -> matchMove.getTeamMemberId().equals(teamMember3.getId())) + .toList(); + var matchMoves4 = match.getMatchMoves().stream() + .filter(matchMove -> matchMove.getTeamMemberId().equals(teamMember4.getId())) + .toList(); + + assertEquals(3, matchMoves1.size()); + assertEquals(1, matchMoves2.size()); + assertEquals(2, matchMoves3.size()); + assertEquals(1, matchMoves4.size()); + + var normalMove11 = matchMoves1.stream().filter(dto -> dto.getMoveId().equals(frstNormalMove.getId())).findFirst().orElseThrow(); + var normalMove12 = matchMoves1.stream().filter(dto -> dto.getMoveId().equals(scndNormalMove.getId())).findFirst().orElseThrow(); + var finishMove1 = matchMoves1.stream().filter(dto -> dto.getMoveId().equals(finishMove.getId())).findFirst().orElseThrow(); + var normalMove21 = matchMoves2.getFirst(); + var normalMove31 = matchMoves3.stream().filter(dto -> dto.getMoveId().equals(frstNormalMove.getId())).findFirst().orElseThrow(); + var normalMove32 = matchMoves3.stream().filter(dto -> dto.getMoveId().equals(scndNormalMove.getId())).findFirst().orElseThrow(); + var normalMove42 = matchMoves4.getFirst(); + + assertNotNull(normalMove11); + assertNotNull(normalMove11.getId()); + assertEquals(teamMember1.getId(), normalMove11.getTeamMemberId()); + assertEquals(frstNormalMove.getId(), normalMove11.getMoveId()); + assertEquals(5, normalMove11.getValue()); + + assertNotNull(normalMove12); + assertNotNull(normalMove12.getId()); + assertEquals(teamMember1.getId(), normalMove12.getTeamMemberId()); + assertEquals(scndNormalMove.getId(), normalMove12.getMoveId()); + assertEquals(2, normalMove12.getValue()); + + assertNotNull(finishMove1); + assertNotNull(finishMove1.getId()); + assertEquals(teamMember1.getId(), finishMove1.getTeamMemberId()); + assertEquals(finishMove.getId(), finishMove1.getMoveId()); + assertEquals(1, finishMove1.getValue()); + + assertNotNull(normalMove21); + assertNotNull(normalMove21.getId()); + assertEquals(teamMember2.getId(), normalMove21.getTeamMemberId()); + assertEquals(frstNormalMove.getId(), normalMove21.getMoveId()); + assertEquals(2, normalMove21.getValue()); + + assertNotNull(normalMove31); + assertNotNull(normalMove31.getId()); + assertEquals(teamMember3.getId(), normalMove31.getTeamMemberId()); + assertEquals(frstNormalMove.getId(), normalMove31.getMoveId()); + assertEquals(4, normalMove31.getValue()); + + assertNotNull(normalMove32); + assertNotNull(normalMove32.getId()); + assertEquals(teamMember3.getId(), normalMove32.getTeamMemberId()); + assertEquals(scndNormalMove.getId(), normalMove32.getMoveId()); + assertEquals(6, normalMove32.getValue()); + + assertNotNull(normalMove42); + assertNotNull(normalMove42.getId()); + assertEquals(teamMember4.getId(), normalMove42.getTeamMemberId()); + assertEquals(scndNormalMove.getId(), normalMove42.getMoveId()); + assertEquals(2, normalMove42.getValue()); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void matches_create_success_onlyFinish() { + var profileNames = List.of("player1", "player2", "player3", "player4"); + var prerequisiteGroup = testUtils.createTestGroup(port, profileNames); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertEquals(profileNames.size(), players.size()); + + var player1 = players.getFirst(); + var player2 = players.get(1); + var player3 = players.get(2); + var player4 = players.get(3); + + assertNotNull(player1); + assertNotNull(player2); + assertNotNull(player3); + assertNotNull(player4); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(finishMove); + assertTrue(finishMove.isFinishingMove()); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(finishMove.getId(), 1) + ), + buildMember( + player2.getId() + ) + ), + buildTeam( + buildMember( + player3.getId() + ), + buildMember( + player4.getId() + ) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var match = requestUtils.assertSuccess(response, MatchDto.class); + + assertNotNull(match.getId()); + assertEquals(prerequisiteGroup.getActiveSeason().getId(), match.getSeason().getId()); + assertEquals(prerequisiteGroup.getCreatedBy(), match.getCreatedBy()); + assertNotNull(match.getDate()); + + assertEquals(2, match.getTeams().size()); + + var team1 = match.getTeams().getFirst(); + var team2 = match.getTeams().getLast(); + + assertNotNull(team1); + assertNotNull(team1.getId()); + assertEquals(match.getId(), team1.getMatchId()); + assertNotNull(team2); + assertNotNull(team2.getId()); + assertEquals(match.getId(), team2.getMatchId()); + + var teamMembers1 = match.getTeamMembers().stream() + .filter(teamMemberDto -> teamMemberDto.getTeamId().equals(team1.getId())) + .toList(); + var teamMembers2 = match.getTeamMembers().stream() + .filter(teamMemberDto -> teamMemberDto.getTeamId().equals(team2.getId())) + .toList(); + + assertEquals(2, teamMembers1.size()); + assertEquals(2, teamMembers2.size()); + + var teamMember1 = teamMembers1.getFirst(); + var teamMember2 = teamMembers1.getLast(); + var teamMember3 = teamMembers2.getFirst(); + var teamMember4 = teamMembers2.getLast(); + + assertNotNull(teamMember1); + assertNotNull(teamMember1.getId()); + assertEquals(team1.getId(), teamMember1.getTeamId()); + assertEquals(player1.getId(), teamMember1.getPlayerId()); + + assertNotNull(teamMember2); + assertNotNull(teamMember2.getId()); + assertEquals(team1.getId(), teamMember2.getTeamId()); + assertEquals(player2.getId(), teamMember2.getPlayerId()); + + assertNotNull(teamMember3); + assertNotNull(teamMember3.getId()); + assertEquals(team2.getId(), teamMember3.getTeamId()); + assertEquals(player3.getId(), teamMember3.getPlayerId()); + + assertNotNull(teamMember4); + assertNotNull(teamMember4.getId()); + assertEquals(team2.getId(), teamMember4.getTeamId()); + assertEquals(player4.getId(), teamMember4.getPlayerId()); + + var matchMoves1 = match.getMatchMoves().stream() + .filter(matchMove -> matchMove.getTeamMemberId().equals(teamMember1.getId())) + .toList(); + + assertEquals(1, matchMoves1.size()); + + var finishMove1 = matchMoves1.stream().filter(dto -> dto.getMoveId().equals(finishMove.getId())).findFirst().orElseThrow(); + + assertNotNull(finishMove1); + assertNotNull(finishMove1.getId()); + assertEquals(teamMember1.getId(), finishMove1.getTeamMemberId()); + assertEquals(finishMove.getId(), finishMove1.getMoveId()); + assertEquals(1, finishMove1.getValue()); + } + + @Test + @Transactional + public void matches_create_invalidSeason() { + var prerequisiteGroup = testUtils.createTestGroup(port); + var oldSeason = prerequisiteGroup.getActiveSeason(); + + var matchDto = buildDto(); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/someIdThatNotExists/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_FOUND); + + var prerequisiteGroup1 = testUtils.createTestGroup(port, List.of("player1", "player2")); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_OF_GROUP); + + var seasonCreateDto = new SeasonCreateDto(); + seasonCreateDto.setOldSeasonName("testing"); + seasonCreateDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var newSeasonResponse = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/active-season", seasonCreateDto, SeasonDto.class); + requestUtils.assertSuccess(newSeasonResponse, SeasonDto.class); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_ALREADY_ENDED); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void matches_create_invalidTeams() { + var prerequisiteGroup = testUtils.createTestGroup(port); + var oldSeason = prerequisiteGroup.getActiveSeason(); + var minTeamSize = oldSeason.getSeasonSettings().getMinTeamSize(); + var maxTeamSize = oldSeason.getSeasonSettings().getMaxTeamSize(); + + assertEquals(1, minTeamSize); + assertEquals(10, maxTeamSize); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertEquals(2, players.size()); + + var player1 = players.getFirst(); + var player2 = players.getLast(); + + var matchDto = buildDto( + buildTeam( + buildMember(player1.getId()) + ), + buildTeam( + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_CREATE_DTO_VALIDATION_FAILED); + + matchDto = buildDto( + buildTeam( + buildMember(player1.getId()) + ), + buildTeam() + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_CREATE_DTO_VALIDATION_FAILED); + + matchDto = buildDto( + buildTeam( + buildMember(player1.getId()) + ), + buildTeam( + buildMember(player2.getId()) + ), + buildTeam( + buildMember(player2.getId()) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_WRONG_AMOUNT_OF_TEAMS); + + matchDto = buildDto( + buildTeam( + buildMember(player1.getId()) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_WRONG_AMOUNT_OF_TEAMS); + + matchDto = buildDto(); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_WRONG_AMOUNT_OF_TEAMS); + } + + @Test + @SuppressWarnings("unchecked") + public void matches_create_nonUniquePlayers() { + var profileNames = List.of("player1", "player2", "player3", "player4"); + var prerequisiteGroup = testUtils.createTestGroup(port, profileNames); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertEquals(profileNames.size(), players.size()); + + var player1 = players.getFirst(); + var player2 = players.get(1); + var player3 = players.get(2); + var player4 = players.get(3); + + assertNotNull(player1); + assertNotNull(player2); + assertNotNull(player3); + assertNotNull(player4); + + // test non unique players in same team + var matchDto = buildDto( + buildTeam( + buildMember(player1.getId()), + buildMember(player2.getId()) + ), + buildTeam( + buildMember(player3.getId()), + buildMember(player3.getId()) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test non unique players in different teams + matchDto = buildDto( + buildTeam( + buildMember(player1.getId()), + buildMember(player3.getId()) + ), + buildTeam( + buildMember(player3.getId()), + buildMember(player4.getId()) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + } + + @Test + @SuppressWarnings("unchecked") + public void matches_create_invalidFinishMove() { + var profileNames = List.of("player1", "player2", "player3", "player4"); + var prerequisiteGroup = testUtils.createTestGroup(port, profileNames); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertEquals(profileNames.size(), players.size()); + + var player1 = players.getFirst(); + var player2 = players.get(1); + var player3 = players.get(2); + var player4 = players.get(3); + + assertNotNull(player1); + assertNotNull(player2); + assertNotNull(player3); + assertNotNull(player4); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMove = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(normalMove); + assertFalse(normalMove.isFinishingMove()); + assertNotNull(finishMove); + assertTrue(finishMove.isFinishingMove()); + + // test too many finish moves in different teams + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 2), + buildMove(finishMove.getId(), 1) + ), + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 2) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(finishMove.getId(), 2) + ), + buildMember( + player4.getId() + ) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test too many finish moves in same team + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 2), + buildMove(finishMove.getId(), 1) + ), + buildMember( + player3.getId(), + buildMove(finishMove.getId(), 2) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 2) + ), + buildMember( + player4.getId() + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test no finish move + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 2) + ), + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 2) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 2), + buildMove(finishMove.getId(), 0) + ), + buildMember( + player4.getId() + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test finish move amount!=1 + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 2) + ), + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 2), + buildMove(finishMove.getId(), 2) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 2) + ), + buildMember( + player4.getId() + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test finish move amount!=1 + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 2) + ), + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 2), + buildMove(finishMove.getId(), 2) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 2) + ), + buildMember( + player4.getId() + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + } + + @Test + @SuppressWarnings("unchecked") + public void matches_create_invalidPlayers() { + var profileNames = List.of("player1", "player2", "player3"); + var prerequisiteGroup = testUtils.createTestGroup(port, profileNames); + var oldSeason = prerequisiteGroup.getActiveSeason(); + + var oldPlayerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/players", List.class, PlayerDto.class); + var oldPlayers = (List) requestUtils.assertSuccess(oldPlayerResponse, ArrayList.class); + + assertEquals(profileNames.size(), oldPlayers.size()); + + var oldPlayer1 = oldPlayers.getFirst(); + var oldPlayer2 = oldPlayers.get(1); + var oldPlayer3 = oldPlayers.get(2); + + assertNotNull(oldPlayer1); + assertNotNull(oldPlayer2); + assertNotNull(oldPlayer3); + + var prerequisiteGroup1 = testUtils.createTestGroup(port, List.of("player1", "player2")); + + var otherPlayerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup1.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var otherPlayers = (List) requestUtils.assertSuccess(otherPlayerResponse, ArrayList.class); + + assertEquals(2, otherPlayers.size()); + + var otherPlayer1 = otherPlayers.getFirst(); + var otherPlayer2 = otherPlayers.getLast(); + + assertNotNull(otherPlayer1); + assertNotNull(otherPlayer2); + + var seasonCreateDto = new SeasonCreateDto(); + seasonCreateDto.setOldSeasonName("testing"); + seasonCreateDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var newSeasonResponse = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/active-season", seasonCreateDto, SeasonDto.class); + var newSeason = requestUtils.assertSuccess(newSeasonResponse, SeasonDto.class); + + var newPlayerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/players", List.class, PlayerDto.class); + var newPlayers = (List) requestUtils.assertSuccess(newPlayerResponse, ArrayList.class); + + assertEquals(profileNames.size(), newPlayers.size()); + + var newPlayer1 = newPlayers.getFirst(); + var newPlayer2 = newPlayers.get(1); + var newPlayer3 = newPlayers.get(2); + + assertNotNull(newPlayer1); + assertNotNull(newPlayer2); + assertNotNull(newPlayer3); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMove = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(normalMove); + assertFalse(normalMove.isFinishingMove()); + assertNotNull(finishMove); + assertTrue(finishMove.isFinishingMove()); + + // test invalid player id + var matchDto = buildDto( + buildTeam( + buildMember(newPlayer1.getId()), + buildMember("someIdThatNotExists") + ), + buildTeam( + buildMember(newPlayer3.getId()) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test invalid player id + matchDto = buildDto( + buildTeam( + buildMember("someIdThatNotExists") + ), + buildTeam( + buildMember(newPlayer3.getId()), + buildMember(newPlayer1.getId()) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test player from other season + matchDto = buildDto( + buildTeam( + buildMember(newPlayer1.getId()), + buildMember(oldPlayer1.getId()) + ), + buildTeam( + buildMember(newPlayer3.getId()) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test player from other season + matchDto = buildDto( + buildTeam( + buildMember(newPlayer1.getId()), + buildMember(newPlayer2.getId()) + ), + buildTeam( + buildMember(oldPlayer1.getId()) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test player from other group + matchDto = buildDto( + buildTeam( + buildMember(newPlayer1.getId()), + buildMember(newPlayer2.getId()) + ), + buildTeam( + buildMember(otherPlayer1.getId()) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test player from other group + matchDto = buildDto( + buildTeam( + buildMember(newPlayer1.getId()), + buildMember(otherPlayer1.getId()) + ), + buildTeam( + buildMember(newPlayer2.getId()) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + } + + @Test + @SuppressWarnings("unchecked") + public void matches_create_invalidMoves() { + var profileNames = List.of("player1", "player2", "player3"); + var prerequisiteGroup = testUtils.createTestGroup(port, profileNames); + var oldSeason = prerequisiteGroup.getActiveSeason(); + + var oldMovesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/rule-moves", List.class, RuleMoveDto.class); + var oldMoves = (List) requestUtils.assertSuccess(oldMovesResponse, ArrayList.class); + + assertTrue(oldMoves.size() >= 2); + + var oldNormalMove = oldMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var oldFinishMove = oldMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(oldNormalMove); + assertFalse(oldNormalMove.isFinishingMove()); + assertNotNull(oldFinishMove); + assertTrue(oldFinishMove.isFinishingMove()); + + var prerequisiteGroup1 = testUtils.createTestGroup(port, List.of("player1", "player2")); + + var otherMovesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup1.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var otherMoves = (List) requestUtils.assertSuccess(otherMovesResponse, ArrayList.class); + + assertTrue(otherMoves.size() >= 2); + + var otherNormalMove = otherMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var otherFinishMove = otherMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(otherNormalMove); + assertFalse(otherNormalMove.isFinishingMove()); + assertNotNull(otherFinishMove); + assertTrue(otherFinishMove.isFinishingMove()); + + var seasonCreateDto = new SeasonCreateDto(); + seasonCreateDto.setOldSeasonName("testing"); + seasonCreateDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var newSeasonResponse = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/active-season", seasonCreateDto, SeasonDto.class); + var newSeason = requestUtils.assertSuccess(newSeasonResponse, SeasonDto.class); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertEquals(profileNames.size(), players.size()); + + var player1 = players.getFirst(); + var player2 = players.get(1); + var player3 = players.get(2); + + assertNotNull(player1); + assertNotNull(player2); + assertNotNull(player3); + + var newMovesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/rule-moves", List.class, RuleMoveDto.class); + var newMoves = (List) requestUtils.assertSuccess(newMovesResponse, ArrayList.class); + + assertTrue(newMoves.size() >= 2); + + var newNormalMove = newMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var newFinishMove = newMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(newNormalMove); + assertFalse(newNormalMove.isFinishingMove()); + assertNotNull(newFinishMove); + assertTrue(newFinishMove.isFinishingMove()); + + // test invalid move id + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove("someIdThatNotExists", 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(newNormalMove.getId(), 2) + ) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test invalid move id + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(newFinishMove.getId(), 1), + buildMove(newNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove("someIdThatNotExists", 2) + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test move from other season + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(newFinishMove.getId(), 1), + buildMove(oldNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(newNormalMove.getId(), 2) + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test move from other season + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(newFinishMove.getId(), 1), + buildMove(newNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(oldNormalMove.getId(), 2) + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test finish move from other season + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(oldFinishMove.getId(), 1), + buildMove(newNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(newNormalMove.getId(), 2) + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test move from other season + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(newNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(oldFinishMove.getId(), 1) + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test move from other group + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(newFinishMove.getId(), 1), + buildMove(otherNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(newNormalMove.getId(), 2) + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test move from other group + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(newFinishMove.getId(), 1), + buildMove(newNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(otherNormalMove.getId(), 2) + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test finish move from other group + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(otherFinishMove.getId(), 1), + buildMove(newNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(newNormalMove.getId(), 2) + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test move from other group + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(newNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(otherFinishMove.getId(), 1) + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void matches_findAll_success() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", List.class, MatchDto.class); + var matches = (List) requestUtils.assertSuccess(response, ArrayList.class); + + assertNotNull(matches); + assertTrue(matches.isEmpty()); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertTrue(players.size() >= 2); + + var player1 = players.getFirst(); + var player2 = players.getLast(); + + assertNotNull(player1); + assertNotNull(player2); + assertNotEquals(player1, player2); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMove = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(normalMove); + assertFalse(normalMove.isFinishingMove()); + assertNotNull(finishMove); + assertTrue(finishMove.isFinishingMove()); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 5), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId(), + buildMove(normalMove.getId(), 3) + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var match1 = requestUtils.assertSuccess(response, MatchDto.class); + + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 9), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId(), + buildMove(normalMove.getId(), 2) + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var match2 = requestUtils.assertSuccess(response, MatchDto.class); + + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 1), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId() + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var match3 = requestUtils.assertSuccess(response, MatchDto.class); + + assertNotNull(match1); + assertNotNull(match2); + assertNotNull(match3); + assertNotEquals(match1.getId(), match2.getId()); + assertNotEquals(match2.getId(), match3.getId()); + assertNotEquals(match1.getId(), match3.getId()); + + match2.setDate(match1.getDate()); + match3.setDate(match1.getDate()); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", List.class, MatchDto.class); + matches = (List) requestUtils.assertSuccess(response, ArrayList.class); + + assertNotNull(matches); + assertFalse(matches.isEmpty()); + assertEquals(3, matches.size()); + + for (MatchDto match : matches) { + match.setDate(match1.getDate()); + assertTrue(List.of(match1, match2, match3).contains(match)); + } + } + + @Test + public void matches_findAll_invalidSeason() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/someIdThatNotExists/matches", List.class, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_FOUND); + + var prerequisiteGroup1 = testUtils.createTestGroup(port, List.of("player1", "player2")); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/matches", List.class, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_OF_GROUP); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void matches_findById_success() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertTrue(players.size() >= 2); + + var player1 = players.getFirst(); + var player2 = players.getLast(); + + assertNotNull(player1); + assertNotNull(player2); + assertNotEquals(player1, player2); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMove = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(normalMove); + assertFalse(normalMove.isFinishingMove()); + assertNotNull(finishMove); + assertTrue(finishMove.isFinishingMove()); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 5), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId(), + buildMove(normalMove.getId(), 3) + ) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var match = requestUtils.assertSuccess(response, MatchDto.class); + + assertNotNull(match); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + match.getId(), MatchDto.class); + var fetched = requestUtils.assertSuccess(response, MatchDto.class); + + assertNotNull(fetched); + + match.setDate(fetched.getDate()); + assertEquals(match, fetched); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void matches_findById_invalidArgs() { + var prerequisiteGroup = testUtils.createTestGroup(port); + var prerequisiteGroup1 = testUtils.createTestGroup(port); + + var response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", List.class, MatchDto.class); + var matches = (List) requestUtils.assertSuccess(response, ArrayList.class); + + assertNotNull(matches); + assertTrue(matches.isEmpty()); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertTrue(players.size() >= 2); + + var player1 = players.getFirst(); + var player2 = players.getLast(); + + assertNotNull(player1); + assertNotNull(player2); + assertNotEquals(player1, player2); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMove = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(normalMove); + assertFalse(normalMove.isFinishingMove()); + assertNotNull(finishMove); + assertTrue(finishMove.isFinishingMove()); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 5), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId(), + buildMove(normalMove.getId(), 3) + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var match = requestUtils.assertSuccess(response, MatchDto.class); + + assertNotNull(match); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/someIdThatNotExists", MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_NOT_FOUND); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup1.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + match.getId(), MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_GROUP_OR_SEASON_ID_DONT_MATCH); + + var seasonDto = new SeasonCreateDto(); + seasonDto.setOldSeasonName("testing"); + seasonDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var newSeasonResponse = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + var newSeason = requestUtils.assertSuccess(newSeasonResponse, SeasonDto.class); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId(), MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_GROUP_OR_SEASON_ID_DONT_MATCH); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void matches_overviewAll_success() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/overview", List.class, MatchOverviewDto.class); + var matches = (List) requestUtils.assertSuccess(response, ArrayList.class); + + assertNotNull(matches); + assertTrue(matches.isEmpty()); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertTrue(players.size() >= 2); + + var player1 = players.getFirst(); + var player2 = players.getLast(); + + assertNotNull(player1); + assertNotNull(player2); + assertNotEquals(player1, player2); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMove = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(normalMove); + assertFalse(normalMove.isFinishingMove()); + assertNotNull(finishMove); + assertTrue(finishMove.isFinishingMove()); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 5), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId(), + buildMove(normalMove.getId(), 3) + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var rawMatch1 = requestUtils.assertSuccess(response, MatchDto.class); + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + rawMatch1.getId() + "/overview", MatchOverviewDto.class); + var match1 = requestUtils.assertSuccess(response, MatchOverviewDto.class); + + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 9), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId(), + buildMove(normalMove.getId(), 2) + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var rawMatch2 = requestUtils.assertSuccess(response, MatchDto.class); + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + rawMatch2.getId() + "/overview", MatchOverviewDto.class); + var match2 = requestUtils.assertSuccess(response, MatchOverviewDto.class); + + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 1), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId() + ) + ) + ); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var rawMatch3 = requestUtils.assertSuccess(response, MatchDto.class); + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + rawMatch3.getId() + "/overview", MatchOverviewDto.class); + var match3 = requestUtils.assertSuccess(response, MatchOverviewDto.class); + + assertNotNull(match1); + assertNotNull(match2); + assertNotNull(match3); + assertNotEquals(match1.getId(), match2.getId()); + assertNotEquals(match2.getId(), match3.getId()); + assertNotEquals(match1.getId(), match3.getId()); + + match2.setDate(match1.getDate()); + match3.setDate(match1.getDate()); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/overview", List.class, MatchOverviewDto.class); + matches = (List) requestUtils.assertSuccess(response, ArrayList.class); + + assertNotNull(matches); + assertFalse(matches.isEmpty()); + assertEquals(3, matches.size()); + + for (MatchOverviewDto match : matches) { + match.setDate(match1.getDate()); + assertTrue(List.of(match1, match2, match3).contains(match)); + } + } + + @Test + public void matches_overviewAll_invalidSeason() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/someIdThatNotExists/matches/overview", List.class, MatchOverviewDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_FOUND); + + var prerequisiteGroup1 = testUtils.createTestGroup(port, List.of("player1", "player2")); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/matches/overview", List.class, MatchOverviewDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_OF_GROUP); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void matches_overviewById_success() { + var profileNames = List.of("profile1", "profile2", "profile3"); + var prerequisiteGroup = testUtils.createTestGroup(port, profileNames); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertTrue(players.size() >= 3); + + var player1 = players.getFirst(); + var player2 = players.get(1); + var player3 = players.get(2); + + assertNotNull(player1); + assertNotNull(player2); + assertNotNull(player3); + assertNotEquals(player1, player2); + assertNotEquals(player2, player3); + assertNotEquals(player1, player3); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMove = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(normalMove); + assertFalse(normalMove.isFinishingMove()); + assertNotNull(finishMove); + assertTrue(finishMove.isFinishingMove()); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 5), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId(), + buildMove(normalMove.getId(), 3) + ), + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 2) + ) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var match = requestUtils.assertSuccess(response, MatchDto.class); + + assertNotNull(match); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + match.getId() + "/overview", MatchOverviewDto.class); + var fetched = requestUtils.assertSuccess(response, MatchOverviewDto.class); + + assertNotNull(fetched); + + assertNotNull(fetched.getDate()); + assertNotNull(fetched.getBlueTeam()); + assertNotNull(fetched.getRedTeam()); + assertEquals(match.getId(), fetched.getId()); + assertEquals(match.getSeason(), fetched.getSeason()); + + assertEquals((normalMove.getPointsForScorer() + normalMove.getPointsForTeam() * fetched.getBlueTeam().getMembers().size()) * 5 + + (finishMove.getPointsForScorer() + finishMove.getPointsForTeam() * fetched.getBlueTeam().getMembers().size()) /* *1 */, fetched.getBlueTeam().getPoints()); + assertEquals((normalMove.getPointsForScorer() + normalMove.getPointsForTeam() * fetched.getRedTeam().getMembers().size()) * 5, fetched.getRedTeam().getPoints()); + + var blueMembers = fetched.getBlueTeam().getMembers(); + var redMembers = fetched.getRedTeam().getMembers(); + + assertEquals(1, blueMembers.size()); + assertEquals(2, redMembers.size()); + + var member1 = blueMembers.getFirst(); + var member2 = redMembers.getFirst(); + var member3 = redMembers.getLast(); + + assertNotNull(member1); + assertNotNull(member2); + assertNotNull(member3); + + assertEquals(player1.getId(), member1.getPlayerId()); + assertEquals((normalMove.getPointsForScorer() + normalMove.getPointsForTeam() * fetched.getBlueTeam().getMembers().size()) * 5 + + (finishMove.getPointsForScorer() + finishMove.getPointsForTeam() * fetched.getBlueTeam().getMembers().size()) /* *1 */, member1.getPoints()); + assertEquals(2, member1.getMoves().size()); + assertEquals(5, member1.getMoves().stream().filter(matchMoveDto -> matchMoveDto.getMoveId().equals(normalMove.getId())).findFirst().orElseThrow().getCount()); + assertEquals(1, member1.getMoves().stream().filter(matchMoveDto -> matchMoveDto.getMoveId().equals(finishMove.getId())).findFirst().orElseThrow().getCount()); + + assertEquals(player2.getId(), member2.getPlayerId()); + assertEquals((normalMove.getPointsForScorer() + normalMove.getPointsForTeam() * fetched.getBlueTeam().getMembers().size()) * 3, member2.getPoints()); + assertEquals(1, member2.getMoves().size()); + assertEquals(3, member2.getMoves().stream().filter(matchMoveDto -> matchMoveDto.getMoveId().equals(normalMove.getId())).findFirst().orElseThrow().getCount()); + + assertEquals(player3.getId(), member3.getPlayerId()); + assertEquals((normalMove.getPointsForScorer() + normalMove.getPointsForTeam() * fetched.getBlueTeam().getMembers().size()) * 2, member3.getPoints()); + assertEquals(1, member3.getMoves().size()); + assertEquals(2, member3.getMoves().stream().filter(matchMoveDto -> matchMoveDto.getMoveId().equals(normalMove.getId())).findFirst().orElseThrow().getCount()); + } + + @Test + @SuppressWarnings("unchecked") + public void matches_overviewById_invalidArgs() { + var prerequisiteGroup = testUtils.createTestGroup(port); + var prerequisiteGroup1 = testUtils.createTestGroup(port); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertTrue(players.size() >= 2); + + var player1 = players.getFirst(); + var player2 = players.getLast(); + + assertNotNull(player1); + assertNotNull(player2); + assertNotEquals(player1, player2); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMove = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(normalMove); + assertFalse(normalMove.isFinishingMove()); + assertNotNull(finishMove); + assertTrue(finishMove.isFinishingMove()); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 5), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId(), + buildMove(normalMove.getId(), 3) + ) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var match = requestUtils.assertSuccess(response, MatchDto.class); + + assertNotNull(match); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/someIdThatNotExists/overview", MatchOverviewDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_NOT_FOUND); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup1.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + match.getId() + "/overview", MatchOverviewDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_GROUP_OR_SEASON_ID_DONT_MATCH); + + var seasonDto = new SeasonCreateDto(); + seasonDto.setOldSeasonName("testing"); + seasonDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var newSeasonResponse = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + var newSeason = requestUtils.assertSuccess(newSeasonResponse, SeasonDto.class); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId() + "/overview", MatchOverviewDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_GROUP_OR_SEASON_ID_DONT_MATCH); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void matches_update_success() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertTrue(players.size() >= 2); + + var player1 = players.getFirst(); + var player2 = players.getLast(); + + assertNotNull(player1); + assertNotNull(player2); + assertNotEquals(player1, player2); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMove = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(normalMove); + assertFalse(normalMove.isFinishingMove()); + assertNotNull(finishMove); + assertTrue(finishMove.isFinishingMove()); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam(buildMember(player2.getId())) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var oldMatch = requestUtils.assertSuccess(response, MatchDto.class); + + assertNotNull(oldMatch); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + oldMatch.getId(), MatchDto.class); + var fetched = requestUtils.assertSuccess(response, MatchDto.class); + + assertNotNull(fetched); + + oldMatch.setDate(fetched.getDate()); + assertEquals(oldMatch, fetched); + + matchDto = buildDto( + fetched, + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 5), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId(), + buildMove(normalMove.getId(), 3) + ) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + oldMatch.getId(), matchDto, MatchDto.class); + var match = requestUtils.assertSuccess(response, MatchDto.class); + + assertEquals(oldMatch.getId(), match.getId()); + assertEquals(oldMatch.getDate(), match.getDate()); + assertEquals(oldMatch.getSeason(), match.getSeason()); + assertEquals(oldMatch.getCreatedBy(), match.getCreatedBy()); + + assertNotNull(match.getId()); + assertEquals(prerequisiteGroup.getActiveSeason().getId(), match.getSeason().getId()); + assertEquals(prerequisiteGroup.getCreatedBy(), match.getCreatedBy()); + assertNotNull(match.getDate()); + + assertEquals(2, match.getTeams().size()); + + var team1 = match.getTeams().getFirst(); + var team2 = match.getTeams().getLast(); + + assertNotNull(team1); + assertNotNull(team1.getId()); + assertEquals(match.getId(), team1.getMatchId()); + assertNotNull(team2); + assertNotNull(team2.getId()); + assertEquals(match.getId(), team2.getMatchId()); + + var teamMembers1 = match.getTeamMembers().stream() + .filter(teamMemberDto -> teamMemberDto.getTeamId().equals(team1.getId())) + .toList(); + var teamMembers2 = match.getTeamMembers().stream() + .filter(teamMemberDto -> teamMemberDto.getTeamId().equals(team2.getId())) + .toList(); + + assertEquals(1, teamMembers1.size()); + assertEquals(1, teamMembers2.size()); + + var teamMember1 = teamMembers1.getFirst(); + var teamMember2 = teamMembers2.getFirst(); + + assertNotNull(teamMember1); + assertNotNull(teamMember1.getId()); + assertEquals(team1.getId(), teamMember1.getTeamId()); + assertEquals(player1.getId(), teamMember1.getPlayerId()); + assertNotNull(teamMember2); + assertNotNull(teamMember2.getId()); + assertEquals(team2.getId(), teamMember2.getTeamId()); + assertEquals(player2.getId(), teamMember2.getPlayerId()); + + var matchMoves1 = match.getMatchMoves().stream() + .filter(matchMove -> matchMove.getTeamMemberId().equals(teamMember1.getId())) + .toList(); + var matchMoves2 = match.getMatchMoves().stream() + .filter(matchMove -> matchMove.getTeamMemberId().equals(teamMember2.getId())) + .toList(); + + assertEquals(2, matchMoves1.size()); + assertEquals(1, matchMoves2.size()); + + var normalMove1 = matchMoves1.stream().filter(dto -> dto.getMoveId().equals(normalMove.getId())).findFirst().orElseThrow(); + var finishMove1 = matchMoves1.stream().filter(dto -> dto.getMoveId().equals(finishMove.getId())).findFirst().orElseThrow(); + var normalMove2 = matchMoves2.getFirst(); + + assertNotNull(normalMove1); + assertNotNull(normalMove1.getId()); + assertEquals(teamMember1.getId(), normalMove1.getTeamMemberId()); + assertEquals(normalMove.getId(), normalMove1.getMoveId()); + assertEquals(5, normalMove1.getValue()); + + assertNotNull(finishMove1); + assertNotNull(finishMove1.getId()); + assertEquals(teamMember1.getId(), finishMove1.getTeamMemberId()); + assertEquals(finishMove.getId(), finishMove1.getMoveId()); + assertEquals(1, finishMove1.getValue()); + + assertNotNull(normalMove2); + assertNotNull(normalMove2.getId()); + assertEquals(teamMember2.getId(), normalMove2.getTeamMemberId()); + assertEquals(normalMove.getId(), normalMove2.getMoveId()); + assertEquals(3, normalMove2.getValue()); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + match.getId(), MatchDto.class); + var newFetched = requestUtils.assertSuccess(response, MatchDto.class); + + assertNotNull(newFetched); + + match.setDate(newFetched.getDate()); + assertEquals(match, newFetched); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void matches_update_invalidSeason() { + var prerequisiteGroup = testUtils.createTestGroup(port); + var oldSeason = prerequisiteGroup.getActiveSeason(); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertTrue(players.size() >= 2); + + var player1 = players.getFirst(); + var player2 = players.getLast(); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMove = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 5), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId(), + buildMove(normalMove.getId(), 3) + ) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var match = requestUtils.assertSuccess(response, MatchDto.class); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/someIdThatNotExists/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_FOUND); + + var prerequisiteGroup1 = testUtils.createTestGroup(port, List.of("player1", "player2")); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_OF_GROUP); + + var seasonCreateDto = new SeasonCreateDto(); + seasonCreateDto.setOldSeasonName("testing"); + seasonCreateDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var newSeasonResponse = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/active-season", seasonCreateDto, SeasonDto.class); + requestUtils.assertSuccess(newSeasonResponse, SeasonDto.class); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_ALREADY_ENDED); + } + + @Test + @SuppressWarnings("unchecked") + public void matches_update_invalidTeams() { + var prerequisiteGroup = testUtils.createTestGroup(port); + var oldSeason = prerequisiteGroup.getActiveSeason(); + var minTeamSize = oldSeason.getSeasonSettings().getMinTeamSize(); + var maxTeamSize = oldSeason.getSeasonSettings().getMaxTeamSize(); + + assertEquals(1, minTeamSize); + assertEquals(10, maxTeamSize); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertTrue(players.size() >= 2); + + var player1 = players.getFirst(); + var player2 = players.getLast(); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMove = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 5), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId(), + buildMove(normalMove.getId(), 3) + ) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var match = requestUtils.assertSuccess(response, MatchDto.class); + + matchDto = buildDto( + buildTeam( + buildMember(player1.getId()) + ), + buildTeam( + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()), + buildMember(player2.getId()) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_CREATE_DTO_VALIDATION_FAILED); + + matchDto = buildDto( + buildTeam( + buildMember(player1.getId()) + ), + buildTeam() + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_CREATE_DTO_VALIDATION_FAILED); + + matchDto = buildDto( + buildTeam( + buildMember(player1.getId()) + ), + buildTeam( + buildMember(player2.getId()) + ), + buildTeam( + buildMember(player2.getId()) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_WRONG_AMOUNT_OF_TEAMS); + + matchDto = buildDto( + buildTeam( + buildMember(player1.getId()) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_WRONG_AMOUNT_OF_TEAMS); + + matchDto = buildDto(); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_WRONG_AMOUNT_OF_TEAMS); + } + + @Test + @SuppressWarnings("unchecked") + public void matches_update_nonUniquePlayers() { + var profileNames = List.of("player1", "player2", "player3", "player4"); + var prerequisiteGroup = testUtils.createTestGroup(port, profileNames); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertEquals(profileNames.size(), players.size()); + + var player1 = players.getFirst(); + var player2 = players.get(1); + var player3 = players.get(2); + var player4 = players.get(3); + + assertNotNull(player1); + assertNotNull(player2); + assertNotNull(player3); + assertNotNull(player4); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMove = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 5), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId(), + buildMove(normalMove.getId(), 3) + ) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var match = requestUtils.assertSuccess(response, MatchDto.class); + + // test non unique players in same team + matchDto = buildDto( + match, + buildTeam( + buildMember(player1.getId()), + buildMember(player2.getId()) + ), + buildTeam( + buildMember(player3.getId()), + buildMember(player3.getId()) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test non unique players in different teams + matchDto = buildDto( + match, + buildTeam( + buildMember(player1.getId()), + buildMember(player3.getId()) + ), + buildTeam( + buildMember(player3.getId()), + buildMember(player4.getId()) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + } + + @Test + @SuppressWarnings("unchecked") + public void matches_update_invalidFinishMove() { + var profileNames = List.of("player1", "player2", "player3", "player4"); + var prerequisiteGroup = testUtils.createTestGroup(port, profileNames); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertEquals(profileNames.size(), players.size()); + + var player1 = players.getFirst(); + var player2 = players.get(1); + var player3 = players.get(2); + var player4 = players.get(3); + + assertNotNull(player1); + assertNotNull(player2); + assertNotNull(player3); + assertNotNull(player4); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMove = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(normalMove); + assertFalse(normalMove.isFinishingMove()); + assertNotNull(finishMove); + assertTrue(finishMove.isFinishingMove()); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 5), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId(), + buildMove(normalMove.getId(), 3) + ) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var match = requestUtils.assertSuccess(response, MatchDto.class); + + // test too many finish moves in different teams + matchDto = buildDto( + match, + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 2), + buildMove(finishMove.getId(), 1) + ), + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 2) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(finishMove.getId(), 2) + ), + buildMember( + player4.getId() + ) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test too many finish moves in same team + matchDto = buildDto( + match, + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 2), + buildMove(finishMove.getId(), 1) + ), + buildMember( + player3.getId(), + buildMove(finishMove.getId(), 2) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 2) + ), + buildMember( + player4.getId() + ) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test no finish move + matchDto = buildDto( + match, + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 2) + ), + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 2) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 2), + buildMove(finishMove.getId(), 0) + ), + buildMember( + player4.getId() + ) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test finish move amount!=1 + matchDto = buildDto( + match, + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 2) + ), + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 2), + buildMove(finishMove.getId(), 2) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 2) + ), + buildMember( + player4.getId() + ) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test finish move amount!=1 + matchDto = buildDto( + match, + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 2) + ), + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 2), + buildMove(finishMove.getId(), 2) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(normalMove.getId(), 2) + ), + buildMember( + player4.getId() + ) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + } + + @Test + @SuppressWarnings("unchecked") + public void matches_update_invalidPlayers() { + var profileNames = List.of("player1", "player2", "player3"); + var prerequisiteGroup = testUtils.createTestGroup(port, profileNames); + var oldSeason = prerequisiteGroup.getActiveSeason(); + + var oldPlayerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/players", List.class, PlayerDto.class); + var oldPlayers = (List) requestUtils.assertSuccess(oldPlayerResponse, ArrayList.class); + + assertEquals(profileNames.size(), oldPlayers.size()); + + var oldPlayer1 = oldPlayers.getFirst(); + var oldPlayer2 = oldPlayers.get(1); + var oldPlayer3 = oldPlayers.get(2); + + assertNotNull(oldPlayer1); + assertNotNull(oldPlayer2); + assertNotNull(oldPlayer3); + + var prerequisiteGroup1 = testUtils.createTestGroup(port, List.of("player1", "player2")); + + var otherPlayerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup1.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var otherPlayers = (List) requestUtils.assertSuccess(otherPlayerResponse, ArrayList.class); + + assertEquals(2, otherPlayers.size()); + + var otherPlayer1 = otherPlayers.getFirst(); + var otherPlayer2 = otherPlayers.getLast(); + + assertNotNull(otherPlayer1); + assertNotNull(otherPlayer2); + + var seasonCreateDto = new SeasonCreateDto(); + seasonCreateDto.setOldSeasonName("testing"); + seasonCreateDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var newSeasonResponse = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/active-season", seasonCreateDto, SeasonDto.class); + var newSeason = requestUtils.assertSuccess(newSeasonResponse, SeasonDto.class); + + var newPlayerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/players", List.class, PlayerDto.class); + var newPlayers = (List) requestUtils.assertSuccess(newPlayerResponse, ArrayList.class); + + assertEquals(profileNames.size(), newPlayers.size()); + + var newPlayer1 = newPlayers.getFirst(); + var newPlayer2 = newPlayers.get(1); + var newPlayer3 = newPlayers.get(2); + + assertNotNull(newPlayer1); + assertNotNull(newPlayer2); + assertNotNull(newPlayer3); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMove = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(normalMove); + assertFalse(normalMove.isFinishingMove()); + assertNotNull(finishMove); + assertTrue(finishMove.isFinishingMove()); + + var matchDto = buildDto( + buildTeam( + buildMember( + newPlayer1.getId(), + buildMove(normalMove.getId(), 5), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + newPlayer2.getId(), + buildMove(normalMove.getId(), 3) + ) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + var match = requestUtils.assertSuccess(response, MatchDto.class); + + // test invalid player id + matchDto = buildDto( + match, + buildTeam( + buildMember(newPlayer1.getId()), + buildMember("someIdThatNotExists") + ), + buildTeam( + buildMember(newPlayer3.getId()) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test invalid player id + matchDto = buildDto( + match, + buildTeam( + buildMember("someIdThatNotExists") + ), + buildTeam( + buildMember(newPlayer3.getId()), + buildMember(newPlayer1.getId()) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test player from other season + matchDto = buildDto( + match, + buildTeam( + buildMember(newPlayer1.getId()), + buildMember(oldPlayer1.getId()) + ), + buildTeam( + buildMember(newPlayer3.getId()) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test player from other season + matchDto = buildDto( + match, + buildTeam( + buildMember(newPlayer1.getId()), + buildMember(newPlayer2.getId()) + ), + buildTeam( + buildMember(oldPlayer1.getId()) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test player from other group + matchDto = buildDto( + match, + buildTeam( + buildMember(newPlayer1.getId()), + buildMember(newPlayer2.getId()) + ), + buildTeam( + buildMember(otherPlayer1.getId()) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test player from other group + matchDto = buildDto( + match, + buildTeam( + buildMember(newPlayer1.getId()), + buildMember(otherPlayer1.getId()) + ), + buildTeam( + buildMember(newPlayer2.getId()) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + } + + @Test + @SuppressWarnings("unchecked") + public void matches_update_invalidMoves() { + var profileNames = List.of("player1", "player2", "player3"); + var prerequisiteGroup = testUtils.createTestGroup(port, profileNames); + var oldSeason = prerequisiteGroup.getActiveSeason(); + + var oldMovesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/rule-moves", List.class, RuleMoveDto.class); + var oldMoves = (List) requestUtils.assertSuccess(oldMovesResponse, ArrayList.class); + + assertTrue(oldMoves.size() >= 2); + + var oldNormalMove = oldMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var oldFinishMove = oldMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(oldNormalMove); + assertFalse(oldNormalMove.isFinishingMove()); + assertNotNull(oldFinishMove); + assertTrue(oldFinishMove.isFinishingMove()); + + var prerequisiteGroup1 = testUtils.createTestGroup(port, List.of("player1", "player2")); + + var otherMovesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup1.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var otherMoves = (List) requestUtils.assertSuccess(otherMovesResponse, ArrayList.class); + + assertTrue(otherMoves.size() >= 2); + + var otherNormalMove = otherMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var otherFinishMove = otherMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(otherNormalMove); + assertFalse(otherNormalMove.isFinishingMove()); + assertNotNull(otherFinishMove); + assertTrue(otherFinishMove.isFinishingMove()); + + var seasonCreateDto = new SeasonCreateDto(); + seasonCreateDto.setOldSeasonName("testing"); + seasonCreateDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var newSeasonResponse = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/active-season", seasonCreateDto, SeasonDto.class); + var newSeason = requestUtils.assertSuccess(newSeasonResponse, SeasonDto.class); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertEquals(profileNames.size(), players.size()); + + var player1 = players.getFirst(); + var player2 = players.get(1); + var player3 = players.get(2); + + assertNotNull(player1); + assertNotNull(player2); + assertNotNull(player3); + + var newMovesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/rule-moves", List.class, RuleMoveDto.class); + var newMoves = (List) requestUtils.assertSuccess(newMovesResponse, ArrayList.class); + + assertTrue(newMoves.size() >= 2); + + var newNormalMove = newMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var newFinishMove = newMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + assertNotNull(newNormalMove); + assertFalse(newNormalMove.isFinishingMove()); + assertNotNull(newFinishMove); + assertTrue(newFinishMove.isFinishingMove()); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 5), + buildMove(newFinishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId(), + buildMove(newNormalMove.getId(), 3) + ) + ) + ); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches", matchDto, MatchDto.class); + var match = requestUtils.assertSuccess(response, MatchDto.class); + + // test invalid move id + matchDto = buildDto( + match, + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove("someIdThatNotExists", 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(newNormalMove.getId(), 2) + ) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test invalid move id + matchDto = buildDto( + match, + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(newFinishMove.getId(), 1), + buildMove(newNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove("someIdThatNotExists", 2) + ) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test move from other season + matchDto = buildDto( + match, + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(newFinishMove.getId(), 1), + buildMove(oldNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(newNormalMove.getId(), 2) + ) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test move from other season + matchDto = buildDto( + match, + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(newFinishMove.getId(), 1), + buildMove(newNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(oldNormalMove.getId(), 2) + ) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test finish move from other season + matchDto = buildDto( + match, + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(oldFinishMove.getId(), 1), + buildMove(newNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(newNormalMove.getId(), 2) + ) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test move from other season + matchDto = buildDto( + match, + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(newNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(oldFinishMove.getId(), 1) + ) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test move from other group + matchDto = buildDto( + match, + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(newFinishMove.getId(), 1), + buildMove(otherNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(newNormalMove.getId(), 2) + ) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test move from other group + matchDto = buildDto( + match, + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(newFinishMove.getId(), 1), + buildMove(newNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(otherNormalMove.getId(), 2) + ) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test finish move from other group + matchDto = buildDto( + match, + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(otherFinishMove.getId(), 1), + buildMove(newNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(newNormalMove.getId(), 2) + ) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + + // test move from other group + matchDto = buildDto( + match, + buildTeam( + buildMember( + player1.getId(), + buildMove(newNormalMove.getId(), 3) + ), + buildMember( + player2.getId(), + buildMove(newNormalMove.getId(), 3) + ) + ), + buildTeam( + buildMember( + player3.getId(), + buildMove(otherFinishMove.getId(), 1) + ) + ) + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + match.getId(), matchDto, MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_DTO_VALIDATION_FAILED); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void matches_delete_success() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertTrue(players.size() >= 2); + + var player1 = players.getFirst(); + var player2 = players.getLast(); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var normalMove = ruleMoves.stream().filter(ruleMoveDto -> !ruleMoveDto.isFinishingMove()).findFirst().orElseThrow(); + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(normalMove.getId(), 5), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam( + buildMember( + player2.getId(), + buildMove(normalMove.getId(), 3) + ) + ) + ); + + var matchResponse = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var match = requestUtils.assertSuccess(matchResponse, MatchDto.class); + + var response = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + match.getId(), null, String.class); + var result = requestUtils.assertSuccess(response, String.class); + + assertEquals("OK", result); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches/" + match.getId(), MatchDto.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_NOT_FOUND); + } + + @Test + @SuppressWarnings("unchecked") + public void matches_delete_invalidSeason() { + var prerequisiteGroup = testUtils.createTestGroup(port); + var oldSeason = prerequisiteGroup.getActiveSeason(); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertTrue(players.size() >= 2); + + var player1 = players.getFirst(); + var player2 = players.getLast(); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam(buildMember(player2.getId())) + ); + + var matchResponse = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var match = requestUtils.assertSuccess(matchResponse, MatchDto.class); + + var response = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/someIdThatNotExists/matches/" + match.getId(), null, String.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_FOUND); + + var prerequisiteGroup1 = testUtils.createTestGroup(port, List.of("player1", "player2")); + + response = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/matches/" + match.getId(), null, String.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_OF_GROUP); + + var seasonCreateDto = new SeasonCreateDto(); + seasonCreateDto.setOldSeasonName("testing"); + seasonCreateDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var newSeasonResponse = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/active-season", seasonCreateDto, SeasonDto.class); + requestUtils.assertSuccess(newSeasonResponse, SeasonDto.class); + + response = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/matches/" + match.getId(), null, String.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_ALREADY_ENDED); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void matches_delete_invalidMatch() { + var prerequisiteGroup = testUtils.createTestGroup(port); + var oldSeason = prerequisiteGroup.getActiveSeason(); + + var playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertTrue(players.size() >= 2); + + var player1 = players.getFirst(); + var player2 = players.getLast(); + + var movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + var finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + var matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam(buildMember(player2.getId())) + ); + + var matchResponse = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var oldMatch = requestUtils.assertSuccess(matchResponse, MatchDto.class); + + // test invalid match id (not existing) + var response = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/matches/someIdThatNotExists", null, String.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_NOT_FOUND); + + // test invalid match id (other season than provided) + var seasonCreateDto = new SeasonCreateDto(); + seasonCreateDto.setOldSeasonName("testing"); + seasonCreateDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var newSeasonResponse = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/active-season", seasonCreateDto, SeasonDto.class); + var newSeason = requestUtils.assertSuccess(newSeasonResponse, SeasonDto.class); + + response = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + oldMatch.getId(), null, String.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_GROUP_OR_SEASON_ID_DONT_MATCH); + + // test invalid match id (other group than provided) + var prerequisiteGroup1 = testUtils.createTestGroup(port); + + playerResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup1.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + players = (List) requestUtils.assertSuccess(playerResponse, ArrayList.class); + + assertTrue(players.size() >= 2); + + player1 = players.getFirst(); + player2 = players.getLast(); + + movesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup1.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + ruleMoves = (List) requestUtils.assertSuccess(movesResponse, ArrayList.class); + + assertTrue(ruleMoves.size() >= 2); + + finishMove = ruleMoves.stream().filter(RuleMoveDto::isFinishingMove).findFirst().orElseThrow(); + + matchDto = buildDto( + buildTeam( + buildMember( + player1.getId(), + buildMove(finishMove.getId(), 1) + ) + ), + buildTeam(buildMember(player2.getId())) + ); + + matchResponse = requestUtils.performPost(port, "/groups/" + prerequisiteGroup1.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/matches", matchDto, MatchDto.class); + var otherGroupMatch = requestUtils.assertSuccess(matchResponse, MatchDto.class); + + response = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/matches/" + otherGroupMatch.getId(), null, String.class); + requestUtils.assertFailure(response, ErrorCodes.MATCH_GROUP_OR_SEASON_ID_DONT_MATCH); + } + + private MatchCreateDto buildDto(TeamCreateDto... teams) { + return buildDto(null, teams); + } + + private MatchCreateDto buildDto(@Nullable MatchDto existing, TeamCreateDto... teams) { + var teamList = List.of(teams); + + if (existing != null) { + for (int i = 0; i < teams.length; i++) { + teamList.get(i).setExistingTeamId(existing.getTeams().get(i).getId()); + } + } + + var dto = new MatchCreateDto(); + dto.setTeams(teamList); + return dto; + } + + private TeamCreateDto buildTeam(TeamMemberCreateDto... members) { + var dto = new TeamCreateDto(); + dto.setTeamMembers(List.of(members)); + return dto; + } + + private TeamMemberCreateDto buildMember(String playerId, MatchMoveDto... moves) { + var dto = new TeamMemberCreateDto(); + dto.setPlayerId(playerId); + dto.setMoves(List.of(moves)); + return dto; + } + + private MatchMoveDto buildMove(String moveId, int amount) { + var dto = new MatchMoveDto(); + dto.setMoveId(moveId); + dto.setCount(amount); + return dto; + } +} \ No newline at end of file diff --git a/api/src/test/java/pro/beerpong/api/control/PlayerControllerTest.java b/api/src/test/java/pro/beerpong/api/control/PlayerControllerTest.java new file mode 100644 index 00000000..b3688484 --- /dev/null +++ b/api/src/test/java/pro/beerpong/api/control/PlayerControllerTest.java @@ -0,0 +1,237 @@ +package pro.beerpong.api.control; + +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; +import pro.beerpong.api.RequestUtils; +import pro.beerpong.api.TestUtils; +import pro.beerpong.api.model.dto.*; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +public class PlayerControllerTest { + @LocalServerPort + private int port; + + @Autowired + private RequestUtils requestUtils; + @Autowired + private TestUtils testUtils; + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void players_copy_seasonStart() { + var profileNames = List.of("player1", "player2", "player3"); + var prerequisiteGroup = testUtils.createTestGroup(port, profileNames); + var oldSeason = prerequisiteGroup.getActiveSeason(); + + var profilesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/profiles", List.class, ProfileDto.class); + var profiles = (List) requestUtils.assertSuccess(profilesResponse, ArrayList.class); + + var oldPlayersResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/players", List.class, PlayerDto.class); + var oldPlayers = (List) requestUtils.assertSuccess(oldPlayersResponse, ArrayList.class); + + var deleteResponse = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/players/" + oldPlayers.getFirst().getId(), null, String.class); + var result = requestUtils.assertSuccess(deleteResponse, String.class); + + assertEquals("OK", result); + + var seasonDto = new SeasonCreateDto(); + seasonDto.setOldSeasonName("testing"); + seasonDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var seasonResponse = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + var newSeason = requestUtils.assertSuccess(seasonResponse, SeasonDto.class); + + var response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(response, ArrayList.class); + + assertEquals(profileNames.size(), profiles.size()); + assertEquals(profiles.size(), oldPlayers.size()); + assertEquals(oldPlayers.size() - 1, players.size()); + assertTrue(oldPlayers.stream().allMatch(playerDto -> profiles.stream().anyMatch(profileDto -> profileDto.getId().equals(playerDto.getProfile().getId())))); + assertTrue(oldPlayers.stream().allMatch(playerDto -> playerDto.getSeason().getId().equals(oldSeason.getId()))); + assertTrue(players.stream().allMatch(playerDto -> profiles.stream().anyMatch(profileDto -> profileDto.getId().equals(playerDto.getProfile().getId())))); + assertTrue(players.stream().allMatch(PlayerDto::isActiveThisSeason)); + assertTrue(players.stream().allMatch(playerDto -> playerDto.getSeason().getId().equals(newSeason.getId()))); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void players_findAll_success() { + var profileNames = List.of("player1", "player2", "player3", "player4"); + var prerequisiteGroup = testUtils.createTestGroup(port, profileNames); + var season = prerequisiteGroup.getActiveSeason(); + + var response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + season.getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(response, ArrayList.class); + + assertEquals(profileNames.size(), players.size()); + + for (PlayerDto playerDto : players) { + assertTrue(profileNames.stream().anyMatch(s -> playerDto.getProfile().getName().equals(s))); + assertTrue(playerDto.isActiveThisSeason()); + assertNull(playerDto.getStatistics()); + assertEquals(season.getId(), playerDto.getSeason().getId()); + } + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + season.getId() + "/players?showStats=true", List.class, PlayerDto.class); + players = (List) requestUtils.assertSuccess(response, ArrayList.class); + + assertEquals(profileNames.size(), players.size()); + + for (PlayerDto playerDto : players) { + assertTrue(profileNames.stream().anyMatch(s -> playerDto.getProfile().getName().equals(s))); + assertTrue(playerDto.isActiveThisSeason()); + assertNotNull(playerDto.getStatistics()); + assertEquals(season.getId(), playerDto.getSeason().getId()); + } + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + season.getId() + "/players?showInactive=true", List.class, PlayerDto.class); + players = (List) requestUtils.assertSuccess(response, ArrayList.class); + + assertEquals(profileNames.size(), players.size()); + + for (PlayerDto playerDto : players) { + assertTrue(profileNames.stream().anyMatch(s -> playerDto.getProfile().getName().equals(s))); + assertTrue(playerDto.isActiveThisSeason()); + assertNull(playerDto.getStatistics()); + assertEquals(season.getId(), playerDto.getSeason().getId()); + } + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + season.getId() + "/players?showInactive=true&showStats=true", List.class, PlayerDto.class); + players = (List) requestUtils.assertSuccess(response, ArrayList.class); + + assertEquals(profileNames.size(), players.size()); + + for (PlayerDto playerDto : players) { + assertTrue(profileNames.stream().anyMatch(s -> playerDto.getProfile().getName().equals(s))); + assertTrue(playerDto.isActiveThisSeason()); + assertNotNull(playerDto.getStatistics()); + assertEquals(season.getId(), playerDto.getSeason().getId()); + } + + var player = players.getFirst(); + + var deleteResponse = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players/" + player.getId(), null, String.class); + var result = requestUtils.assertSuccess(deleteResponse, String.class); + + assertEquals("OK", result); + + var allPlayersResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + season.getId() + "/players?showInactive=true", List.class, PlayerDto.class); + var allPlayers = (List) requestUtils.assertSuccess(allPlayersResponse, ArrayList.class); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + season.getId() + "/players", List.class, PlayerDto.class); + players = (List) requestUtils.assertSuccess(response, ArrayList.class); + + assertEquals(players.size() + 1, allPlayers.size()); + assertTrue(allPlayers.stream().anyMatch(s -> s.getId().equals(player.getId()))); + assertFalse(players.stream().anyMatch(s -> s.getId().equals(player.getId()))); + } + + @Test + @Transactional + public void players_findAll_invalidSeason() { + var prerequisiteGroup = testUtils.createTestGroup(port, List.of("player1", "player2")); + + var response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/someIdThatNotExists/players", List.class, PlayerDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_FOUND); + + var prerequisiteGroup1 = testUtils.createTestGroup(port, List.of("player1", "player2")); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_OF_GROUP); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void players_delete_success() { + var profileNames = List.of("player1", "player2"); + var prerequisiteGroup = testUtils.createTestGroup(port, profileNames); + + var playersResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playersResponse, ArrayList.class); + + assertEquals(profileNames.size(), players.size()); + + var player = players.getFirst(); + + assertEquals(profileNames.getFirst(), player.getProfile().getName()); + + var response = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players/" + player.getId(), null, String.class); + var result = requestUtils.assertSuccess(response, String.class); + + assertEquals("OK", result); + + var newPlayersResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var newPlayers = (List) requestUtils.assertSuccess(newPlayersResponse, ArrayList.class); + + assertFalse(newPlayers.isEmpty()); + assertEquals(players.size(), newPlayers.size() + 1); + assertTrue(newPlayers.stream().noneMatch(playerDto -> playerDto.getProfile().getId().equals(player.getProfile().getId()))); + + var first = newPlayers.getFirst(); + + response = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players/" + first.getId(), null, String.class); + result = requestUtils.assertSuccess(response, String.class); + + assertEquals("OK", result); + + newPlayersResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + newPlayers = (List) requestUtils.assertSuccess(newPlayersResponse, ArrayList.class); + + assertTrue(newPlayers.isEmpty()); + } + + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void players_delete_invalidArgs() { + var prerequisiteGroup = testUtils.createTestGroup(port, List.of("player1", "player2")); + + var playersResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playersResponse, ArrayList.class); + var player = players.getFirst(); + + var response = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players/someIdThatNotExists", null, String.class); + requestUtils.assertFailure(response, ErrorCodes.PLAYER_NOT_FOUND); + + response = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/someIdThatNotExists/players/" + player.getId(), null, String.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_FOUND); + + var prerequisiteGroup1 = testUtils.createTestGroup(port, List.of("player1", "player2")); + + response = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/players/" + player.getId(), null, String.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_OF_GROUP); + + var otherPlayersResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup1.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/players", List.class, PlayerDto.class); + var otherPlayers = (List) requestUtils.assertSuccess(otherPlayersResponse, ArrayList.class); + var otherPlayer = otherPlayers.getFirst(); + + response = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players/" + otherPlayer.getId(), null, String.class); + requestUtils.assertFailure(response, ErrorCodes.PLAYER_VALIDATION_FAILED); + + response = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players/" + player.getId(), null, String.class); + var result = requestUtils.assertSuccess(response, String.class); + + assertEquals("OK", result); + + response = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/players/" + player.getId(), null, String.class); + requestUtils.assertFailure(response, ErrorCodes.PLAYER_ALREADY_DELETED); + } +} \ No newline at end of file diff --git a/api/src/test/java/pro/beerpong/api/control/ProfileControllerTest.java b/api/src/test/java/pro/beerpong/api/control/ProfileControllerTest.java new file mode 100644 index 00000000..3290bd7f --- /dev/null +++ b/api/src/test/java/pro/beerpong/api/control/ProfileControllerTest.java @@ -0,0 +1,216 @@ +package pro.beerpong.api.control; + +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; +import pro.beerpong.api.RequestUtils; +import pro.beerpong.api.TestUtils; +import pro.beerpong.api.model.dto.*; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +public class ProfileControllerTest { + @LocalServerPort + private int port; + + @Autowired + private RequestUtils requestUtils; + @Autowired + private TestUtils testUtils; + + @Test + @SuppressWarnings("unchecked") + public void profiles_create_groupCreation() { + var profileNames = List.of("player1", "player2", "player3"); + var prerequisiteGroup = testUtils.createTestGroup(port, "test", profileNames); + + var response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/profiles", List.class, ProfileDto.class); + var profiles = (List) requestUtils.assertSuccess(response, ArrayList.class); + + assertEquals(profileNames.size(), profiles.size()); + assertTrue(profileNames.stream().allMatch(s -> profiles.stream().anyMatch(profileDto -> profileDto.getName().equals(s)))); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void profiles_create_success() { + var profileNames = List.of("player1", "player2", "player3"); + var prerequisiteGroup = testUtils.createTestGroup(port, profileNames); + var oldSeason = prerequisiteGroup.getActiveSeason(); + + // test creation of profile with a non existing name + var profileDto = new ProfileCreateDto(); + profileDto.setName("testing"); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/profiles", profileDto, ProfileCreatedDto.class); + var result = requestUtils.assertSuccess(response, ProfileCreatedDto.class); + + assertEquals(profileDto.getName(), result.getName()); + assertEquals(prerequisiteGroup.getId(), result.getGroupId()); + assertEquals(prerequisiteGroup.getCreatedBy(), result.getCreatedBy()); + assertNull(result.getAvatarAsset()); + + assertFalse(result.isReactivated()); + assertNull(result.getLastActiveSeasonId()); + + // test reactivating of players when player is deleted + var playersResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/players", List.class, PlayerDto.class); + var players = (List) requestUtils.assertSuccess(playersResponse, ArrayList.class); + var player = players.getFirst(); + + assertTrue(profileNames.stream().anyMatch(s -> player.getProfile().getName().equals(s))); + + var deleteResponse = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/players/" + player.getId(), null, String.class); + var deleteResult = requestUtils.assertSuccess(deleteResponse, String.class); + + assertEquals("OK", deleteResult); + + profileDto.setName(player.getProfile().getName()); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/profiles", profileDto, ProfileCreatedDto.class); + result = requestUtils.assertSuccess(response, ProfileCreatedDto.class); + + assertEquals(profileDto.getName(), result.getName()); + assertEquals(prerequisiteGroup.getId(), result.getGroupId()); + + assertTrue(result.isReactivated()); + assertEquals(result.getLastActiveSeasonId(), oldSeason.getId()); + + // test linking to old profile if no player exists in the current season + playersResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/players", List.class, PlayerDto.class); + players = (List) requestUtils.assertSuccess(playersResponse, ArrayList.class); + var first = players.getFirst(); + + deleteResponse = requestUtils.performDelete(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/players/" + first.getId(), null, String.class); + deleteResult = requestUtils.assertSuccess(deleteResponse, String.class); + + assertEquals("OK", deleteResult); + + var seasonDto = new SeasonCreateDto(); + seasonDto.setOldSeasonName("testing"); + seasonDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var seasonResponse = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + requestUtils.assertSuccess(seasonResponse, SeasonDto.class); + + profileDto.setName(first.getProfile().getName()); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/profiles", profileDto, ProfileCreatedDto.class); + result = requestUtils.assertSuccess(response, ProfileCreatedDto.class); + + assertEquals(profileDto.getName(), result.getName()); + assertEquals(prerequisiteGroup.getId(), result.getGroupId()); + + assertFalse(result.isReactivated()); + assertEquals(result.getLastActiveSeasonId(), oldSeason.getId()); + } + + @Test + @Transactional + public void profiles_create_alreadyExists() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var profileDto = new ProfileCreateDto(); + profileDto.setName("player1"); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/profiles", profileDto, ProfileCreatedDto.class); + requestUtils.assertFailure(response, ErrorCodes.PROFILE_ALREADY_EXISTS); + } + + @Test + @SuppressWarnings("unchecked") + public void profiles_findById_success() { + var profileNames = List.of("player1", "player2", "player3"); + var prerequisiteGroup = testUtils.createTestGroup(port, "test", profileNames); + + var response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/profiles", List.class, ProfileDto.class); + var profiles = (List) requestUtils.assertSuccess(response, ArrayList.class); + + for (ProfileDto profile : profiles) { + var prfleResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/profiles/" + profile.getId(), ProfileDto.class); + var fetched = requestUtils.assertSuccess(prfleResponse, ProfileDto.class); + + assertEquals(profile, fetched); + } + } + + @Test + @SuppressWarnings("unchecked") + public void profiles_findById_invalidProfile() { + var profileNames = List.of("player1", "player2", "player3"); + var prerequisiteGroup = testUtils.createTestGroup(port, "test", profileNames); + + var response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/profiles/someIdThatNotExists", ProfileDto.class); + requestUtils.assertFailure(response, ErrorCodes.PROFILE_NOT_FOUND); + + var prflResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/profiles", List.class, ProfileDto.class); + var profiles = (List) requestUtils.assertSuccess(prflResponse, ArrayList.class); + + assertFalse(profiles.isEmpty()); + + var prerequisiteGroup1 = testUtils.createTestGroup(port, "test", profileNames); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup1.getId() + "/profiles/" + profiles.getFirst().getId(), ProfileDto.class); + requestUtils.assertFailure(response, ErrorCodes.PROFILE_NOT_OF_GROUP); + } + + @Test + @SuppressWarnings("unchecked") + public void profiles_update_success() { + var profileNames = List.of("player1", "player2"); + var prerequisiteGroup = testUtils.createTestGroup(port, "test", profileNames); + + var prflResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/profiles", List.class, ProfileDto.class); + var profiles = (List) requestUtils.assertSuccess(prflResponse, ArrayList.class); + + assertFalse(profiles.isEmpty()); + + var oldProfile = profiles.getFirst(); + + var profileDto = new ProfileCreateDto(); + profileDto.setName(oldProfile.getName() + "test"); + + var response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/profiles/" + oldProfile.getId(), profileDto, ProfileDto.class); + var newProfile = requestUtils.assertSuccess(response, ProfileDto.class); + + assertEquals(profileDto.getName(), newProfile.getName()); + assertEquals(oldProfile.getId(), newProfile.getId()); + assertEquals(oldProfile.getCreatedBy(), newProfile.getCreatedBy()); + assertEquals(oldProfile.getGroupId(), newProfile.getGroupId()); + } + + @Test + @SuppressWarnings("unchecked") + public void profiles_update_invalidProfile() { + var profileNames = List.of("player1", "player2", "player3"); + var prerequisiteGroup = testUtils.createTestGroup(port, "test", profileNames); + + var profileDto = new ProfileCreateDto(); + profileDto.setName("testing"); + + var response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/profiles/someIdThatNotExists", profileDto, ProfileDto.class); + requestUtils.assertFailure(response, ErrorCodes.PROFILE_NOT_FOUND); + + var prflResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/profiles", List.class, ProfileDto.class); + var profiles = (List) requestUtils.assertSuccess(prflResponse, ArrayList.class); + + assertFalse(profiles.isEmpty()); + + var prerequisiteGroup1 = testUtils.createTestGroup(port, "test", profileNames); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup1.getId() + "/profiles/" + profiles.getFirst().getId(), profileDto, ProfileDto.class); + requestUtils.assertFailure(response, ErrorCodes.PROFILE_NOT_FOUND); + } +} \ No newline at end of file diff --git a/api/src/test/java/pro/beerpong/api/control/RuleControllerTest.java b/api/src/test/java/pro/beerpong/api/control/RuleControllerTest.java new file mode 100644 index 00000000..975f1283 --- /dev/null +++ b/api/src/test/java/pro/beerpong/api/control/RuleControllerTest.java @@ -0,0 +1,219 @@ +package pro.beerpong.api.control; + +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; +import pro.beerpong.api.RequestUtils; +import pro.beerpong.api.TestUtils; +import pro.beerpong.api.model.dto.*; +import pro.beerpong.api.service.RuleService; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +public class RuleControllerTest { + @LocalServerPort + private int port; + + @Autowired + private RequestUtils requestUtils; + @Autowired + private TestUtils testUtils; + + @Test + @SuppressWarnings("unchecked") + public void rules_create_groupCreation() { + var prerequisiteGroup = testUtils.createTestGroup(port, "test", "beerpong"); + + var response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rules", List.class, RuleDto.class); + var rules = (List) requestUtils.assertSuccess(response, ArrayList.class); + + testUtils.assertRulesEquals(RuleService.DEFAULT_RULES, rules); + + prerequisiteGroup = testUtils.createTestGroup(port, "test", "kicker"); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rules", List.class, RuleDto.class); + rules = (List) requestUtils.assertSuccess(response, ArrayList.class); + + assertTrue(rules.isEmpty()); + + prerequisiteGroup = testUtils.createTestGroup(port, "test", null, "test123"); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rules", List.class, RuleDto.class); + rules = (List) requestUtils.assertSuccess(response, ArrayList.class); + + assertTrue(rules.isEmpty()); + } + + @Test + public void rules_findAll_invalidSeason() { + var prerequisiteGroup = testUtils.createTestGroup(port, "test", "beerpong"); + var season = prerequisiteGroup.getActiveSeason(); + + var response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/someIdThatNotExists/rules", List.class, RuleDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_FOUND); + + var prerequisiteGroup1 = testUtils.createTestGroup(port, "test", "beerpong"); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup1.getId() + "/seasons/" + season.getId() + "/rules", List.class, RuleDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_OF_GROUP); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void rules_copy_seasonStart() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var oldResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rules", List.class, RuleDto.class); + var oldRules = (List) requestUtils.assertSuccess(oldResponse, ArrayList.class); + + var seasonCreateDto = new SeasonCreateDto(); + seasonCreateDto.setOldSeasonName("testing"); + seasonCreateDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var newSeasonResponse = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/active-season", seasonCreateDto, SeasonDto.class); + requestUtils.assertSuccess(newSeasonResponse, SeasonDto.class); + + var newResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rules", List.class, RuleDto.class); + var newRules = (List) requestUtils.assertSuccess(newResponse, ArrayList.class); + + assertEquals(oldRules.size(), newRules.size()); + testUtils.assertRulesEquals(oldRules, newRules); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void rules_update_success() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var oldResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rules", List.class, RuleDto.class); + var oldRules = (List) requestUtils.assertSuccess(oldResponse, ArrayList.class); + + var createdRules = List.of( + buildRule("rule1", "descr1"), + buildRule("rule2", "descr2"), + buildRule("rule3", "descr3") + ); + + assertNotEquals(oldRules.size(), createdRules.size()); + + var response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rules", createdRules, List.class, RuleDto.class); + var newRules = (List) requestUtils.assertSuccess(response, ArrayList.class); + + assertNotEquals(oldRules.size(), newRules.size()); + assertEquals(createdRules.size(), newRules.size()); + testUtils.assertCreatedRulesEquals(createdRules, newRules); + } + + @Test + public void rules_update_invalidSeason() { + var prerequisiteGroup = testUtils.createTestGroup(port); + var oldSeason = prerequisiteGroup.getActiveSeason(); + + var createdRules = List.of( + buildRule("rule1", "descr1"), + buildRule("rule2", "descr2"), + buildRule("rule3", "descr3") + ); + + var response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/someIdThatNotExists/rules", createdRules, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_FOUND); + + var prerequisiteGroup1 = testUtils.createTestGroup(port); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/rules", createdRules, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_OF_GROUP); + + var seasonCreateDto = new SeasonCreateDto(); + seasonCreateDto.setOldSeasonName("testing"); + seasonCreateDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var newSeasonResponse = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/active-season", seasonCreateDto, SeasonDto.class); + requestUtils.assertSuccess(newSeasonResponse, SeasonDto.class); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/rules", createdRules, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_ALREADY_ENDED); + } + + @Test + public void rules_update_invalidDto() { + var prerequisiteGroup = testUtils.createTestGroup(port); + var oldSeason = prerequisiteGroup.getActiveSeason(); + + var createdRules = List.of( + buildRule("rule1", "descr1"), + buildRule("rule2", "descr2"), + buildRule(null, "descr3") + ); + + var response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rules", createdRules, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.RULE_INVALID_DTO); + + createdRules = List.of( + buildRule("rule1", "descr1"), + buildRule("rule2", "descr2"), + buildRule("", "descr3") + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rules", createdRules, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.RULE_INVALID_DTO); + + createdRules = List.of( + buildRule("rule1", "descr1"), + buildRule("rule2", "descr2"), + buildRule(" ", "descr3") + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rules", createdRules, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.RULE_INVALID_DTO); + + createdRules = List.of( + buildRule("rule1", "descr1"), + buildRule("rule2", null), + buildRule("rul3", "descr3") + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rules", createdRules, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.RULE_INVALID_DTO); + + createdRules = List.of( + buildRule("rule1", "descr1"), + buildRule("rule2", ""), + buildRule("rul3", "descr3") + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rules", createdRules, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.RULE_INVALID_DTO); + + createdRules = List.of( + buildRule("rule1", "descr1"), + buildRule("rule2", " "), + buildRule("rul3", "descr3") + ); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rules", createdRules, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.RULE_INVALID_DTO); + } + + private RuleCreateDto buildRule(String title, String descr) { + var ruleCreateDto = new RuleCreateDto(); + ruleCreateDto.setTitle(title); + ruleCreateDto.setDescription(descr); + return ruleCreateDto; + } +} \ No newline at end of file diff --git a/api/src/test/java/pro/beerpong/api/control/RuleMoveControllerTest.java b/api/src/test/java/pro/beerpong/api/control/RuleMoveControllerTest.java new file mode 100644 index 00000000..1147a5b6 --- /dev/null +++ b/api/src/test/java/pro/beerpong/api/control/RuleMoveControllerTest.java @@ -0,0 +1,299 @@ +package pro.beerpong.api.control; + +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; +import pro.beerpong.api.RequestUtils; +import pro.beerpong.api.TestUtils; +import pro.beerpong.api.model.dto.*; +import pro.beerpong.api.service.RuleMoveService; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +public class RuleMoveControllerTest { + @LocalServerPort + private int port; + + @Autowired + private RequestUtils requestUtils; + @Autowired + private TestUtils testUtils; + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void ruleMoves_create_groupCreation() { + var prerequisiteGroup = testUtils.createTestGroup(port, "test", "beerpong"); + + var response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(response, ArrayList.class); + + testUtils.assertRuleMovesEquals(RuleMoveService.DEFAULT_BEERPONG_MOVES, ruleMoves); + + prerequisiteGroup = testUtils.createTestGroup(port, "test", "kicker"); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + ruleMoves = (List) requestUtils.assertSuccess(response, ArrayList.class); + + testUtils.assertRuleMovesEquals(RuleMoveService.DEFAULT_MOVES, ruleMoves); + + prerequisiteGroup = testUtils.createTestGroup(port, "test", null, "test123"); + + response = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + ruleMoves = (List) requestUtils.assertSuccess(response, ArrayList.class); + + testUtils.assertRuleMovesEquals(RuleMoveService.DEFAULT_MOVES, ruleMoves); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void ruleMoves_create_seasonStart() { + var prerequisiteGroup = testUtils.createTestGroup(port, "test", "beerpong"); + + var oldResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var oldRuleMoves = (List) requestUtils.assertSuccess(oldResponse, ArrayList.class); + + var createRuleMoves = List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + ); + + assertNotEquals(oldRuleMoves.size(), createRuleMoves.size()); + + var seasonCreateDto = new SeasonCreateDto(); + seasonCreateDto.setOldSeasonName("testing"); + seasonCreateDto.setRuleMoves(createRuleMoves); + + var newSeasonResponse = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/active-season", seasonCreateDto, SeasonDto.class); + var newSeason = requestUtils.assertSuccess(newSeasonResponse, SeasonDto.class); + + var newResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + newSeason.getId() + "/rule-moves", List.class, RuleMoveDto.class); + var newRuleMoves = (List) requestUtils.assertSuccess(newResponse, ArrayList.class); + + assertNotEquals(oldRuleMoves.size(), newRuleMoves.size()); + testUtils.assertCreatedRuleMovesEquals(createRuleMoves, newRuleMoves); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void ruleMoves_create_success() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var allMovesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var oldRuleMoves = (List) requestUtils.assertSuccess(allMovesResponse, ArrayList.class); + + var ruleMoveDto = new RuleMoveCreateDto(); + ruleMoveDto.setName("testing"); + ruleMoveDto.setFinishingMove(false); + ruleMoveDto.setPointsForScorer(3); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", ruleMoveDto, RuleMoveDto.class); + var ruleMove = requestUtils.assertSuccess(response, RuleMoveDto.class); + + allMovesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var ruleMoves = (List) requestUtils.assertSuccess(allMovesResponse, ArrayList.class); + + assertEquals(oldRuleMoves.size() + 1, ruleMoves.size()); + assertTrue(ruleMoves.contains(ruleMove)); + assertFalse(oldRuleMoves.contains(ruleMove)); + testUtils.assertCreatedRuleMoveEquals(ruleMoveDto, ruleMove); + } + + @Test + public void ruleMoves_create_invalidSeason() { + var prerequisiteGroup = testUtils.createTestGroup(port); + var oldSeason = prerequisiteGroup.getActiveSeason(); + + var ruleMoveDto = new RuleMoveCreateDto(); + ruleMoveDto.setName("testing"); + ruleMoveDto.setFinishingMove(false); + ruleMoveDto.setPointsForScorer(3); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/someIdThatNotExists/rule-moves", ruleMoveDto, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_FOUND); + + var prerequisiteGroup1 = testUtils.createTestGroup(port); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/rule-moves", ruleMoveDto, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_OF_GROUP); + + var seasonCreateDto = new SeasonCreateDto(); + seasonCreateDto.setOldSeasonName("testing"); + seasonCreateDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var newSeasonResponse = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/active-season", seasonCreateDto, SeasonDto.class); + requestUtils.assertSuccess(newSeasonResponse, SeasonDto.class); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/rule-moves", ruleMoveDto, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_ALREADY_ENDED); + } + + @Test + public void ruleMoves_create_invalidDto() { + var prerequisiteGroup = testUtils.createTestGroup(port); + var oldSeason = prerequisiteGroup.getActiveSeason(); + + var ruleMoveDto = new RuleMoveCreateDto(); + ruleMoveDto.setName(null); + + var response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/rule-moves", ruleMoveDto, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.RULE_MOVE_INVALID_DTO); + + ruleMoveDto.setName(""); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/rule-moves", ruleMoveDto, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.RULE_MOVE_INVALID_DTO); + + ruleMoveDto.setName(" "); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/rule-moves", ruleMoveDto, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.RULE_MOVE_INVALID_DTO); + + ruleMoveDto.setName("test"); + ruleMoveDto.setPointsForTeam(-1); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/rule-moves", ruleMoveDto, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.RULE_MOVE_INVALID_DTO); + + ruleMoveDto.setName("test"); + ruleMoveDto.setPointsForTeam(0); + ruleMoveDto.setPointsForScorer(-1); + + response = requestUtils.performPost(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/rule-moves", ruleMoveDto, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.RULE_MOVE_INVALID_DTO); + } + + @Test + @Transactional + @SuppressWarnings("unchecked") + public void ruleMoves_update_success() { + var prerequisiteGroup = testUtils.createTestGroup(port); + + var allMovesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var oldRuleMoves = (List) requestUtils.assertSuccess(allMovesResponse, ArrayList.class); + + assertFalse(oldRuleMoves.isEmpty()); + + var oldRuleMove = oldRuleMoves.getFirst(); + + var ruleMoveDto = new RuleMoveCreateDto(); + ruleMoveDto.setName("testing"); + ruleMoveDto.setPointsForScorer(3); + ruleMoveDto.setPointsForScorer(4); + + var response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves/" + oldRuleMove.getId(), ruleMoveDto, RuleMoveDto.class); + var ruleMove = requestUtils.assertSuccess(response, RuleMoveDto.class); + + assertEquals(oldRuleMove.getId(), ruleMove.getId()); + assertNotEquals(oldRuleMove, ruleMove); + testUtils.assertCreatedRuleMoveEquals(ruleMoveDto, ruleMove); + } + + @Test + @SuppressWarnings("unchecked") + public void ruleMoves_update_invalidSeason() { + var prerequisiteGroup = testUtils.createTestGroup(port); + var oldSeason = prerequisiteGroup.getActiveSeason(); + + var allMovesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var oldRuleMoves = (List) requestUtils.assertSuccess(allMovesResponse, ArrayList.class); + + assertFalse(oldRuleMoves.isEmpty()); + + var oldRuleMove = oldRuleMoves.getFirst(); + + var ruleMoveDto = new RuleMoveCreateDto(); + ruleMoveDto.setName("testing"); + ruleMoveDto.setFinishingMove(false); + ruleMoveDto.setPointsForScorer(3); + + var response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/someIdThatNotExists/rule-moves/" + oldRuleMove.getId(), ruleMoveDto, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_FOUND); + + var prerequisiteGroup1 = testUtils.createTestGroup(port); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup1.getActiveSeason().getId() + "/rule-moves/" + oldRuleMove.getId(), ruleMoveDto, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_OF_GROUP); + + var seasonCreateDto = new SeasonCreateDto(); + seasonCreateDto.setOldSeasonName("testing"); + seasonCreateDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var newSeasonResponse = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/active-season", seasonCreateDto, SeasonDto.class); + requestUtils.assertSuccess(newSeasonResponse, SeasonDto.class); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/rule-moves/" + oldRuleMove.getId(), ruleMoveDto, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_ALREADY_ENDED); + } + + @Test + @SuppressWarnings("unchecked") + public void ruleMoves_update_invalidDto() { + var prerequisiteGroup = testUtils.createTestGroup(port); + var oldSeason = prerequisiteGroup.getActiveSeason(); + + var allMovesResponse = requestUtils.performGet(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + prerequisiteGroup.getActiveSeason().getId() + "/rule-moves", List.class, RuleMoveDto.class); + var oldRuleMoves = (List) requestUtils.assertSuccess(allMovesResponse, ArrayList.class); + + assertFalse(oldRuleMoves.isEmpty()); + + var oldRuleMove = oldRuleMoves.getFirst(); + + var ruleMoveDto = new RuleMoveCreateDto(); + ruleMoveDto.setName(null); + + var response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/rule-moves/" + oldRuleMove.getId(), ruleMoveDto, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.RULE_MOVE_INVALID_DTO); + + ruleMoveDto.setName(""); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/rule-moves/" + oldRuleMove.getId(), ruleMoveDto, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.RULE_MOVE_INVALID_DTO); + + ruleMoveDto.setName(" "); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/rule-moves/" + oldRuleMove.getId(), ruleMoveDto, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.RULE_MOVE_INVALID_DTO); + + ruleMoveDto.setName("test"); + ruleMoveDto.setPointsForTeam(-1); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/rule-moves/" + oldRuleMove.getId(), ruleMoveDto, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.RULE_MOVE_INVALID_DTO); + + ruleMoveDto.setName("test"); + ruleMoveDto.setPointsForTeam(0); + ruleMoveDto.setPointsForScorer(-1); + + response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/rule-moves/" + oldRuleMove.getId(), ruleMoveDto, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.RULE_MOVE_INVALID_DTO); + } + + @Test + public void ruleMoves_update_invalidRuleMove() { + var prerequisiteGroup = testUtils.createTestGroup(port); + var oldSeason = prerequisiteGroup.getActiveSeason(); + + var ruleMoveDto = new RuleMoveCreateDto(); + ruleMoveDto.setName("test"); + + var response = requestUtils.performPut(port, "/groups/" + prerequisiteGroup.getId() + "/seasons/" + oldSeason.getId() + "/rule-moves/someIdThatNotExists", ruleMoveDto, RuleMoveDto.class); + requestUtils.assertFailure(response, ErrorCodes.RULE_MOVE_NOT_FOUND); + } +} \ No newline at end of file diff --git a/api/src/test/java/pro/beerpong/api/control/SeasonControllerTest.java b/api/src/test/java/pro/beerpong/api/control/SeasonControllerTest.java new file mode 100644 index 00000000..e4011cf8 --- /dev/null +++ b/api/src/test/java/pro/beerpong/api/control/SeasonControllerTest.java @@ -0,0 +1,441 @@ +package pro.beerpong.api.control; + +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; +import pro.beerpong.api.TestUtils; +import pro.beerpong.api.RequestUtils; +import pro.beerpong.api.model.dao.SeasonSettings; +import pro.beerpong.api.model.dto.*; +import pro.beerpong.api.util.DailyLeaderboard; +import pro.beerpong.api.util.RankingAlgorithm; + +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +public class SeasonControllerTest { + @LocalServerPort + private int port; + + @Autowired + private RequestUtils requestUtils; + @Autowired + private TestUtils testUtils; + + @Test + public void season_findById_success() { + var prerequisteGroup = testUtils.createTestGroup(port); + + var response = requestUtils.performGet(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + prerequisteGroup.getActiveSeason().getId(), SeasonDto.class); + var season = requestUtils.assertSuccess(response, SeasonDto.class); + + testUtils.assertSeasonEquals(prerequisteGroup.getActiveSeason(), season); + } + + @Test + public void season_findById_invalidId() { + var prerequisteGroup1 = testUtils.createTestGroup(port); + + var response = requestUtils.performGet(port, "/groups/" + prerequisteGroup1.getId() + "/seasons/someIdThatNotExists", SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_FOUND); + + var prerequisteGroup2 = testUtils.createTestGroup(port); + + // season form other group + response = requestUtils.performGet(port, "/groups/" + prerequisteGroup1.getId() + "/seasons/" + prerequisteGroup2.getActiveSeason().getId(), SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_FOUND); + } + + @Test + @Transactional + public void season_start_success() { + var prerequisteGroup = testUtils.createTestGroup(port); + var oldSeason = prerequisteGroup.getActiveSeason(); + + var seasonDto = new SeasonCreateDto(); + seasonDto.setOldSeasonName("testing"); + seasonDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + var newSeason = requestUtils.assertSuccess(response, SeasonDto.class); + + var groupResponse = requestUtils.performGet(port, "/groups/" + prerequisteGroup.getId(), GroupDto.class); + var updatedGroup = requestUtils.assertSuccess(groupResponse, GroupDto.class); + + var oldSeasonResponse = requestUtils.performGet(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + oldSeason.getId(), SeasonDto.class); + var updatedOldSeason = requestUtils.assertSuccess(oldSeasonResponse, SeasonDto.class); + + // test active season in group + testUtils.assertSeasonEquals(updatedGroup.getActiveSeason(), newSeason); + assertNotEquals(oldSeason.getId(), newSeason.getId()); + + // test new season + assertNull(newSeason.getName()); + assertEquals(newSeason.getGroupId(), prerequisteGroup.getId()); + assertEquals(newSeason.getCreatedBy(), oldSeason.getCreatedBy()); + assertEquals(requestUtils.currentUserId(), newSeason.getCreatedBy().getUserId()); + assertNotNull(newSeason.getStartDate()); + assertNull(newSeason.getEndDate()); + + // test copying of season settings + assertEquals(newSeason.getSeasonSettings().getMaxTeamSize(), updatedOldSeason.getSeasonSettings().getMaxTeamSize()); + assertEquals(newSeason.getSeasonSettings().getMinTeamSize(), updatedOldSeason.getSeasonSettings().getMinTeamSize()); + assertEquals(newSeason.getSeasonSettings().getMinMatchesToQualify(), updatedOldSeason.getSeasonSettings().getMinMatchesToQualify()); + assertEquals(newSeason.getSeasonSettings().getRankingAlgorithm(), updatedOldSeason.getSeasonSettings().getRankingAlgorithm()); + assertEquals(newSeason.getSeasonSettings().getDailyLeaderboard(), updatedOldSeason.getSeasonSettings().getDailyLeaderboard()); + assertEquals(newSeason.getSeasonSettings().getWakeTime(), updatedOldSeason.getSeasonSettings().getWakeTime()); + + // test changes to old season + assertNotNull(updatedOldSeason.getName()); + assertNotNull(updatedOldSeason.getEndDate()); + assertEquals(seasonDto.getOldSeasonName(), updatedOldSeason.getName()); + } + + @Test + public void season_start_invalidName() { + var prerequisteGroup = testUtils.createTestGroup(port); + + var seasonDto = new SeasonCreateDto(); + seasonDto.setOldSeasonName(null); + seasonDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_SEASON_NAME); + + seasonDto.setOldSeasonName(""); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_SEASON_NAME); + + seasonDto.setOldSeasonName(" "); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_SEASON_NAME); + + seasonDto.setOldSeasonName("a"); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_SEASON_NAME); + + seasonDto.setOldSeasonName("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_SEASON_NAME); + } + + @Test + public void season_start_invalidRuleMoves() { + var prerequisteGroup = testUtils.createTestGroup(port); + + var seasonDto = new SeasonCreateDto(); + seasonDto.setOldSeasonName("testing"); + seasonDto.setRuleMoves(null); + + var response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_RULE_MOVES); + + seasonDto.setRuleMoves(List.of()); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_RULE_MOVES); + + seasonDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0) + )); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_RULE_MOVES); + + seasonDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Finish", true, 1, 0) + )); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_RULE_MOVES); + + seasonDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove(null, true, 1, 0) + )); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_RULE_MOVES); + + seasonDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("", true, 1, 0) + )); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_RULE_MOVES); + + seasonDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove(" ", true, 1, 0) + )); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_RULE_MOVES); + + seasonDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("test", true, -1, 0) + )); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_RULE_MOVES); + + seasonDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("test", true, 0, -1) + )); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_RULE_MOVES); + } + + @Test + @SuppressWarnings("unchecked") + public void season_findAll() { + var prerequisteGroup = testUtils.createTestGroup(port); + + var response = requestUtils.performGet(port, "/groups/" + prerequisteGroup.getId() + "/seasons", List.class, SeasonDto.class); + var seasons = (List) requestUtils.assertSuccess(response, ArrayList.class); + + assertEquals(1, seasons.size()); + + var firstSeason = seasons.getFirst(); + + testUtils.assertSeasonEquals(firstSeason, prerequisteGroup.getActiveSeason()); + + var seasonDto = new SeasonCreateDto(); + seasonDto.setOldSeasonName("testing"); + seasonDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + var newSeasonResponse = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/active-season", seasonDto, SeasonDto.class); + var newSeason = requestUtils.assertSuccess(newSeasonResponse, SeasonDto.class); + + var oldSeasonResponse = requestUtils.performGet(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + firstSeason.getId(), SeasonDto.class); + var updatedOldSeason = requestUtils.assertSuccess(oldSeasonResponse, SeasonDto.class); + + response = requestUtils.performGet(port, "/groups/" + prerequisteGroup.getId() + "/seasons", List.class, SeasonDto.class); + seasons = (List) requestUtils.assertSuccess(response, ArrayList.class); + + assertEquals(2, seasons.size()); + testUtils.assertSeasonEquals(newSeason, seasons.stream().filter(toCheck -> toCheck.getId().equals(newSeason.getId())).findFirst().orElse(null)); + testUtils.assertSeasonEquals(updatedOldSeason, seasons.stream().filter(toCheck -> toCheck.getId().equals(firstSeason.getId())).findFirst().orElse(null)); + } + + @Test + @Transactional + public void season_update_success() { + var prerequisteGroup = testUtils.createTestGroup(port); + var oldSeason = prerequisteGroup.getActiveSeason(); + + var seasonDto = buildUpdateDto(seasonSettings -> { + seasonSettings.setDailyLeaderboard(DailyLeaderboard.LAST_24_HOURS); + seasonSettings.setMinTeamSize(3); + }); + + var response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + oldSeason.getId(), seasonDto, SeasonDto.class); + var season = requestUtils.assertSuccess(response, SeasonDto.class); + + // test that rest of group is the same + assertEquals(oldSeason.getCreatedBy(), season.getCreatedBy()); + assertEquals(requestUtils.currentUserId(), season.getCreatedBy().getUserId()); + assertEquals(oldSeason.getEndDate(), season.getEndDate()); + assertEquals(oldSeason.getName(), season.getName()); + assertEquals(oldSeason.getGroupId(), season.getGroupId()); + + assertEquals(DailyLeaderboard.LAST_24_HOURS, season.getSeasonSettings().getDailyLeaderboard()); + assertEquals(oldSeason.getSeasonSettings().getRankingAlgorithm(), season.getSeasonSettings().getRankingAlgorithm()); + assertEquals(3, season.getSeasonSettings().getMinTeamSize()); + assertEquals(oldSeason.getSeasonSettings().getMaxTeamSize(), season.getSeasonSettings().getMaxTeamSize()); + assertEquals(oldSeason.getSeasonSettings().getMinMatchesToQualify(), season.getSeasonSettings().getMinMatchesToQualify()); + assertEquals(oldSeason.getSeasonSettings().getWakeTime(), season.getSeasonSettings().getWakeTime()); + + seasonDto = buildUpdateDto(seasonSettings -> { + seasonSettings.setMaxTeamSize(5); + }); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + oldSeason.getId(), seasonDto, SeasonDto.class); + season = requestUtils.assertSuccess(response, SeasonDto.class); + + assertEquals(DailyLeaderboard.LAST_24_HOURS, season.getSeasonSettings().getDailyLeaderboard()); + assertEquals(oldSeason.getSeasonSettings().getRankingAlgorithm(), season.getSeasonSettings().getRankingAlgorithm()); + assertEquals(3, season.getSeasonSettings().getMinTeamSize()); + assertEquals(5, season.getSeasonSettings().getMaxTeamSize()); + assertEquals(oldSeason.getSeasonSettings().getMinMatchesToQualify(), season.getSeasonSettings().getMinMatchesToQualify()); + assertEquals(oldSeason.getSeasonSettings().getWakeTime(), season.getSeasonSettings().getWakeTime()); + + seasonDto = buildUpdateDto(seasonSettings -> { + seasonSettings.setMinMatchesToQualify(7); + seasonSettings.setMaxTeamSize(5); + }); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + oldSeason.getId(), seasonDto, SeasonDto.class); + season = requestUtils.assertSuccess(response, SeasonDto.class); + + assertEquals(DailyLeaderboard.LAST_24_HOURS, season.getSeasonSettings().getDailyLeaderboard()); + assertEquals(oldSeason.getSeasonSettings().getRankingAlgorithm(), season.getSeasonSettings().getRankingAlgorithm()); + assertEquals(3, season.getSeasonSettings().getMinTeamSize()); + assertEquals(5, season.getSeasonSettings().getMaxTeamSize()); + assertEquals(7, season.getSeasonSettings().getMinMatchesToQualify()); + assertEquals(oldSeason.getSeasonSettings().getWakeTime(), season.getSeasonSettings().getWakeTime()); + + seasonDto = buildUpdateDto(seasonSettings -> { + seasonSettings.setRankingAlgorithm(RankingAlgorithm.ELO); + seasonSettings.setWakeTime("09:33"); + }); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + oldSeason.getId(), seasonDto, SeasonDto.class); + season = requestUtils.assertSuccess(response, SeasonDto.class); + + assertEquals(DailyLeaderboard.LAST_24_HOURS, season.getSeasonSettings().getDailyLeaderboard()); + assertEquals(RankingAlgorithm.ELO, season.getSeasonSettings().getRankingAlgorithm()); + assertEquals(3, season.getSeasonSettings().getMinTeamSize()); + assertEquals(5, season.getSeasonSettings().getMaxTeamSize()); + assertEquals(7, season.getSeasonSettings().getMinMatchesToQualify()); + assertEquals(LocalTime.of(9, 33), season.getSeasonSettings().getWakeTime()); + } + + @Test + @Transactional + public void season_update_invalidDto() { + var prerequisteGroup = testUtils.createTestGroup(port); + + var seasonDto = new SeasonUpdateDto(); + + var response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + prerequisteGroup.getActiveSeason().getId(), seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.INVALID_SEASON_DTO); + } + + @Test + @Transactional + public void season_update_invalidSeason() { + var prerequisteGroup = testUtils.createTestGroup(port); + var oldSeason = prerequisteGroup.getActiveSeason(); + + var seasonDto = this.buildUpdateDto(seasonSettings -> {}); + + // test season id that not exists + var response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/someIdThatNotExists", seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_FOUND); + + var prerequisteGroup2 = testUtils.createTestGroup(port); + + // test season id from other group than in path + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + prerequisteGroup2.getActiveSeason().getId(), seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_NOT_OF_GROUP); + + var seasonCreateDto = new SeasonCreateDto(); + seasonCreateDto.setOldSeasonName("testing"); + seasonCreateDto.setRuleMoves(List.of( + testUtils.buildRuleMove("Normal", false, 1, 0), + testUtils.buildRuleMove("Finish", true, 1, 3) + )); + + // test season id from already ended season + var newSeasonResponse = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/active-season", seasonCreateDto, SeasonDto.class); + requestUtils.assertSuccess(newSeasonResponse, SeasonDto.class); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + oldSeason.getId(), seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_ALREADY_ENDED); + } + + @Test + @Transactional + public void season_update_invalidWakeTimeFormat() { + var prerequisteGroup = testUtils.createTestGroup(port); + + var seasonDto = this.buildUpdateDto(seasonSettings -> seasonSettings.setWakeTime("")); + + var response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + prerequisteGroup.getActiveSeason().getId(), seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_WRONG_TIME_FORMAT); + + seasonDto = this.buildUpdateDto(seasonSettings -> seasonSettings.setWakeTime("-1")); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + prerequisteGroup.getActiveSeason().getId(), seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_WRONG_TIME_FORMAT); + + seasonDto = this.buildUpdateDto(seasonSettings -> seasonSettings.setWakeTime("XX:xx")); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + prerequisteGroup.getActiveSeason().getId(), seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_WRONG_TIME_FORMAT); + + seasonDto = this.buildUpdateDto(seasonSettings -> seasonSettings.setWakeTime("-02:00")); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + prerequisteGroup.getActiveSeason().getId(), seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_WRONG_TIME_FORMAT); + + seasonDto = this.buildUpdateDto(seasonSettings -> seasonSettings.setWakeTime("25:35")); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + prerequisteGroup.getActiveSeason().getId(), seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_WRONG_TIME_FORMAT); + + seasonDto = this.buildUpdateDto(seasonSettings -> seasonSettings.setWakeTime("14:61")); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + prerequisteGroup.getActiveSeason().getId(), seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_WRONG_TIME_FORMAT); + } + + @Test + @Transactional + public void season_update_invalidTeamSizes() { + var prerequisteGroup = testUtils.createTestGroup(port); + + var seasonDto = this.buildUpdateDto(seasonSettings -> { + seasonSettings.setMinTeamSize(3); + seasonSettings.setMaxTeamSize(7); + }); + + var response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + prerequisteGroup.getActiveSeason().getId(), seasonDto, SeasonDto.class); + requestUtils.assertSuccess(response, SeasonDto.class); + + seasonDto = this.buildUpdateDto(seasonSettings -> seasonSettings.setMaxTeamSize(2)); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + prerequisteGroup.getActiveSeason().getId(), seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_WRONG_TEAM_SIZES); + + seasonDto = this.buildUpdateDto(seasonSettings -> seasonSettings.setMinTeamSize(8)); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + prerequisteGroup.getActiveSeason().getId(), seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_WRONG_TEAM_SIZES); + + seasonDto = this.buildUpdateDto(seasonSettings -> { + seasonSettings.setMinTeamSize(8); + seasonSettings.setMaxTeamSize(7); + }); + + response = requestUtils.performPut(port, "/groups/" + prerequisteGroup.getId() + "/seasons/" + prerequisteGroup.getActiveSeason().getId(), seasonDto, SeasonDto.class); + requestUtils.assertFailure(response, ErrorCodes.SEASON_WRONG_TEAM_SIZES); + } + + private SeasonUpdateDto buildUpdateDto(Consumer consumer) { + var seasonDto = new SeasonUpdateDto(); + var seasonSettings = new SeasonSettingsDto(); + + consumer.accept(seasonSettings); + seasonDto.setSeasonSettings(seasonSettings); + + return seasonDto; + } +} \ No newline at end of file diff --git a/api/src/test/java/pro/beerpong/api/util/EloTest.java b/api/src/test/java/pro/beerpong/api/util/EloTest.java index b6d8a565..0d27dd91 100644 --- a/api/src/test/java/pro/beerpong/api/util/EloTest.java +++ b/api/src/test/java/pro/beerpong/api/util/EloTest.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.stream.Collectors; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import com.google.api.client.util.Lists; @@ -27,6 +28,7 @@ public class EloTest { private static final Gson GSON = new GsonBuilder().create(); @Test + @Disabled public void testElo() { var classLoader = getClass().getClassLoader(); diff --git a/docker/docker-compose-dev.yml b/docker/docker-compose-dev.yml index b4a82536..748435a3 100644 --- a/docker/docker-compose-dev.yml +++ b/docker/docker-compose-dev.yml @@ -5,6 +5,7 @@ services: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB_NAME} + POSTGRES_PORT: ${POSTGRES_PORT} volumes: - postgres:/var/lib/postgresql/data networks: @@ -17,7 +18,7 @@ services: environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_URL: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB_NAME} + POSTGRES_URL: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB_NAME}?user=${POSTGRES_USER}&password=${POSTGRES_PASSWORD} API_BASE_URL: ${API_BASE_URL} # Sentry BACKEND_SENTRY_DSN: ${BACKEND_SENTRY_DSN} @@ -27,6 +28,7 @@ services: AWS_ENDPOINT: ${AWS_ENDPOINT} AWS_ACCESS_KEY: ${AWS_ACCESS_KEY} AWS_SECRET_KEY: ${AWS_SECRET_KEY} + JWT_SECRET: ${JWT_SECRET} networks: - backend volumes: diff --git a/mobile-app/api/calls/groupHooks.ts b/mobile-app/api/calls/groupHooks.ts index ff6ebcb6..3a11c83f 100644 --- a/mobile-app/api/calls/groupHooks.ts +++ b/mobile-app/api/calls/groupHooks.ts @@ -36,6 +36,7 @@ export const useGroupQuery = (id: ApiId | null) => { export const useJoinGroupMutation = () => { const { api } = useApi(); + return useMutation< Paths.FindGroupByInviteCode.Responses.$200 | null, Error, @@ -43,14 +44,44 @@ export const useJoinGroupMutation = () => { >({ mutationFn: async (inviteCode) => { const res = await (await api).findGroupByInviteCode({ inviteCode }); + + if (res.data.data) { + await (await api).joinGroup({ id: res.data.data.id! }, {}); + } return res?.data; }, onError: captureMutationErr('joinGroup'), }); }; +export const useLeaveGroupMutation = () => { + const { api } = useApi(); + + return useMutation({ + mutationFn: async (id) => { + const res = await (await api).leaveGroup({ id }); + + return res?.data; + }, + }); +}; + +export const useGetMyGroupsQuery = () => { + const { api } = useApi(); + + return useQuery({ + queryFn: async () => { + const res = await (await api).findUserGroups(); + + return res?.data; + }, + queryKey: [QK.group, 'myGroups'], + }); +}; + export const useCreateGroupMutation = () => { const { api } = useApi(); + return useMutation< Paths.CreateGroup.Responses.$200 | null, Error, diff --git a/mobile-app/api/calls/matchHooks.ts b/mobile-app/api/calls/matchHooks.ts index d706b486..88dd08ea 100644 --- a/mobile-app/api/calls/matchHooks.ts +++ b/mobile-app/api/calls/matchHooks.ts @@ -23,6 +23,7 @@ export const useMatchQuery = ( if (!groupId || !seasonId || !matchId) { return null; } + const res = await ( await api ).getMatchById({ groupId, seasonId, id: matchId }); diff --git a/mobile-app/api/generated/openapi.json b/mobile-app/api/generated/openapi.json index 94454db0..ef521b0e 100644 --- a/mobile-app/api/generated/openapi.json +++ b/mobile-app/api/generated/openapi.json @@ -382,7 +382,7 @@ "schema": { "type": "string" } }, { - "name": "seasonId", + "name": "teamId", "in": "path", "required": true, "schema": { "type": "string" } @@ -394,7 +394,7 @@ "schema": { "type": "string" } }, { - "name": "teamId", + "name": "seasonId", "in": "path", "required": true, "schema": { "type": "string" } @@ -424,7 +424,7 @@ "schema": { "type": "string" } }, { - "name": "seasonId", + "name": "teamId", "in": "path", "required": true, "schema": { "type": "string" } @@ -436,7 +436,7 @@ "schema": { "type": "string" } }, { - "name": "teamId", + "name": "seasonId", "in": "path", "required": true, "schema": { "type": "string" } @@ -759,6 +759,58 @@ } } }, + "/groups/{id}/leave": { + "post": { + "tags": ["group-controller"], + "operationId": "leaveGroup", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseEnvelopeString" + } + } + } + } + } + } + }, + "/groups/{id}/join": { + "post": { + "tags": ["group-controller"], + "operationId": "joinGroup", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseEnvelopeString" + } + } + } + } + } + } + }, "/groups/{groupId}/seasons/{seasonId}/rule-moves": { "get": { "tags": ["rule-move-controller"], @@ -963,6 +1015,62 @@ } } }, + "/auth/signup": { + "post": { + "tags": ["auth-controller"], + "operationId": "signup", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthSignupDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseEnvelopeAuthTokenDto" + } + } + } + } + } + } + }, + "/auth/refresh": { + "post": { + "tags": ["auth-controller"], + "operationId": "refreshAuth", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthRefreshDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseEnvelopeAuthTokenDto" + } + } + } + } + } + } + }, "/healthcheck": { "get": { "tags": ["healthcheck-controller"], @@ -1159,6 +1267,24 @@ } } }, + "/groups/user": { + "get": { + "tags": ["group-controller"], + "operationId": "findUserGroups", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseEnvelopeListGroupDto" + } + } + } + } + } + } + }, "/group-presets": { "get": { "tags": ["group-presets-controller"], @@ -1287,10 +1413,15 @@ "id": { "type": "string" }, "name": { "type": "string" }, "inviteCode": { "type": "string" }, - "activeSeason": { "$ref": "#/components/schemas/Season" }, + "activeSeason": { + "$ref": "#/components/schemas/SeasonDto" + }, "wallpaperAsset": { "$ref": "#/components/schemas/AssetMetadataDto" }, + "createdBy": { + "$ref": "#/components/schemas/GroupMemberDto" + }, "createdAt": { "type": "string", "format": "date-time" }, "sportPreset": { "$ref": "#/components/schemas/GroupPreset" @@ -1301,6 +1432,15 @@ "numberOfSeasons": { "type": "integer", "format": "int32" } } }, + "GroupMemberDto": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "active": { "type": "boolean" }, + "groupId": { "type": "string" }, + "userId": { "type": "string" } + } + }, "GroupPreset": { "type": "object", "properties": { @@ -1309,6 +1449,15 @@ "imageUrl": { "type": "string" } } }, + "LocalTime": { + "type": "object", + "properties": { + "hour": { "type": "integer", "format": "int32" }, + "minute": { "type": "integer", "format": "int32" }, + "second": { "type": "integer", "format": "int32" }, + "nano": { "type": "integer", "format": "int32" } + } + }, "ResponseEnvelopeGroupDto": { "type": "object", "properties": { @@ -1318,7 +1467,7 @@ "error": { "$ref": "#/components/schemas/ErrorDetails" } } }, - "Season": { + "SeasonDto": { "type": "object", "properties": { "id": { "type": "string" }, @@ -1328,6 +1477,9 @@ "groupId": { "type": "string" }, "seasonSettings": { "$ref": "#/components/schemas/SeasonSettings" + }, + "createdBy": { + "$ref": "#/components/schemas/GroupMemberDto" } } }, @@ -1353,7 +1505,7 @@ "LAST_24_HOURS" ] }, - "wakeTimeHour": { "type": "integer", "format": "int32" } + "wakeTime": { "$ref": "#/components/schemas/LocalTime" } } }, "AssetCropDto": { @@ -1398,7 +1550,10 @@ "id": { "type": "string" }, "title": { "type": "string" }, "description": { "type": "string" }, - "season": { "$ref": "#/components/schemas/Season" } + "season": { "$ref": "#/components/schemas/SeasonDto" }, + "createdBy": { + "$ref": "#/components/schemas/GroupMemberDto" + } } }, "RuleMoveCreateDto": { @@ -1427,7 +1582,7 @@ "pointsForTeam": { "type": "integer", "format": "int32" }, "pointsForScorer": { "type": "integer", "format": "int32" }, "finishingMove": { "type": "boolean" }, - "season": { "$ref": "#/components/schemas/Season" } + "season": { "$ref": "#/components/schemas/SeasonDto" } } }, "MatchCreateDto": { @@ -1476,7 +1631,10 @@ "properties": { "id": { "type": "string" }, "date": { "type": "string", "format": "date-time" }, - "season": { "$ref": "#/components/schemas/Season" }, + "season": { "$ref": "#/components/schemas/SeasonDto" }, + "createdBy": { + "$ref": "#/components/schemas/GroupMemberDto" + }, "teams": { "type": "array", "items": { "$ref": "#/components/schemas/TeamDto" } @@ -1540,12 +1698,36 @@ "error": { "$ref": "#/components/schemas/ErrorDetails" } } }, + "SeasonSettingsDto": { + "type": "object", + "properties": { + "minMatchesToQualify": { + "type": "integer", + "format": "int32" + }, + "minTeamSize": { "type": "integer", "format": "int32" }, + "maxTeamSize": { "type": "integer", "format": "int32" }, + "rankingAlgorithm": { + "type": "string", + "enum": ["AVERAGE", "ELO"] + }, + "dailyLeaderboard": { + "type": "string", + "enum": [ + "RESET_AT_MIDNIGHT", + "WAKE_TIME", + "LAST_24_HOURS" + ] + }, + "wakeTime": { "type": "string" } + } + }, "SeasonUpdateDto": { "required": ["seasonSettings"], "type": "object", "properties": { "seasonSettings": { - "$ref": "#/components/schemas/SeasonSettings" + "$ref": "#/components/schemas/SeasonSettingsDto" } } }, @@ -1558,19 +1740,6 @@ "error": { "$ref": "#/components/schemas/ErrorDetails" } } }, - "SeasonDto": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "startDate": { "type": "string", "format": "date-time" }, - "endDate": { "type": "string", "format": "date-time" }, - "groupId": { "type": "string" }, - "seasonSettings": { - "$ref": "#/components/schemas/SeasonSettings" - } - } - }, "ProfileCreateDto": { "type": "object", "properties": { "name": { "type": "string" } } @@ -1583,7 +1752,10 @@ "avatarAsset": { "$ref": "#/components/schemas/AssetMetadataDto" }, - "groupId": { "type": "string" } + "groupId": { "type": "string" }, + "createdBy": { + "$ref": "#/components/schemas/GroupMemberDto" + } } }, "ResponseEnvelopeProfileDto": { @@ -1607,6 +1779,15 @@ } } }, + "ResponseEnvelopeString": { + "type": "object", + "properties": { + "status": { "type": "string", "enum": ["OK", "ERROR"] }, + "httpCode": { "type": "integer", "format": "int32" }, + "data": { "type": "string" }, + "error": { "$ref": "#/components/schemas/ErrorDetails" } + } + }, "ProfileCreatedDto": { "type": "object", "properties": { @@ -1616,6 +1797,9 @@ "$ref": "#/components/schemas/AssetMetadataDto" }, "groupId": { "type": "string" }, + "createdBy": { + "$ref": "#/components/schemas/GroupMemberDto" + }, "reactivated": { "type": "boolean" }, "lastActiveSeasonId": { "type": "string" } } @@ -1631,15 +1815,36 @@ "error": { "$ref": "#/components/schemas/ErrorDetails" } } }, - "ResponseEnvelopeString": { + "AuthSignupDto": { + "type": "object", + "properties": { + "installationType": { + "type": "string", + "enum": ["IOS", "ANDROID"] + }, + "deviceId": { "type": "string" } + } + }, + "AuthTokenDto": { + "type": "object", + "properties": { + "token": { "type": "string" }, + "type": { "type": "string", "enum": ["ACCESS", "REFRESH"] } + } + }, + "ResponseEnvelopeAuthTokenDto": { "type": "object", "properties": { "status": { "type": "string", "enum": ["OK", "ERROR"] }, "httpCode": { "type": "integer", "format": "int32" }, - "data": { "type": "string" }, + "data": { "$ref": "#/components/schemas/AuthTokenDto" }, "error": { "$ref": "#/components/schemas/ErrorDetails" } } }, + "AuthRefreshDto": { + "type": "object", + "properties": { "refreshToken": { "type": "string" } } + }, "ResponseEnvelopeListSeasonDto": { "type": "object", "properties": { @@ -1730,7 +1935,7 @@ "properties": { "id": { "type": "string" }, "date": { "type": "string", "format": "date-time" }, - "season": { "$ref": "#/components/schemas/Season" }, + "season": { "$ref": "#/components/schemas/SeasonDto" }, "blueTeam": { "$ref": "#/components/schemas/MatchOverviewTeamDto" }, @@ -1818,6 +2023,18 @@ "error": { "$ref": "#/components/schemas/ErrorDetails" } } }, + "ResponseEnvelopeListGroupDto": { + "type": "object", + "properties": { + "status": { "type": "string", "enum": ["OK", "ERROR"] }, + "httpCode": { "type": "integer", "format": "int32" }, + "data": { + "type": "array", + "items": { "$ref": "#/components/schemas/GroupDto" } + }, + "error": { "$ref": "#/components/schemas/ErrorDetails" } + } + }, "ResponseEnvelopeListGroupPreset": { "type": "object", "properties": { diff --git a/mobile-app/api/propHooks/groupPropHooks.ts b/mobile-app/api/propHooks/groupPropHooks.ts index 245794b9..fd3f4e2a 100644 --- a/mobile-app/api/propHooks/groupPropHooks.ts +++ b/mobile-app/api/propHooks/groupPropHooks.ts @@ -1,3 +1,4 @@ +import { useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'expo-router'; import { @@ -17,11 +18,13 @@ import { launchImageLibrary } from '@/utils/fileUpload'; import { ConsoleLogger } from '@/utils/logging'; import { useGroupStore } from '@/zustand/group/stateGroupStore'; +import { QK } from '../utils/reactQuery'; + export const useGroupSettingsProps = (): ScreenState => { const router = useRouter(); const { groupId, group } = useGroup(); - const { removeGroup } = useGroupStore(); + const { leaveGroupMutation } = useGroupStore(); const seasonsQuery = useAllSeasonsQuery(groupId); @@ -72,19 +75,31 @@ export const useGroupSettingsProps = (): ScreenState => { } } - function onLeaveGroup() { + const queryClient = useQueryClient(); + + async function onLeaveGroup() { if (!groupId) return; - // TODO: i can't get this to actually show up - setTimeout( - () => showYouLeftGroupToast(group.data?.name ?? 'Unknown Group'), - 3000 - ); + try { + // TODO: i can't get this to actually show up + setTimeout( + () => + showYouLeftGroupToast(group.data?.name ?? 'Unknown Group'), + 3000 + ); - removeGroup(groupId); + await leaveGroupMutation.mutateAsync(groupId); - router.dismissAll(); - router.replace('/'); + await queryClient.invalidateQueries({ + queryKey: [QK.group, 'myGroups'], + }); + + router.dismissAll(); + router.replace('/'); + } catch (err) { + ConsoleLogger.error('failed to leave group:', err); + showErrorToast('Failed to leave group.'); + } } const props: GroupSettingsProps | null = data?.data diff --git a/mobile-app/api/realtime/useRealtimeConnection.ts b/mobile-app/api/realtime/useRealtimeConnection.ts index 72d22ca5..893cb8af 100644 --- a/mobile-app/api/realtime/useRealtimeConnection.ts +++ b/mobile-app/api/realtime/useRealtimeConnection.ts @@ -10,16 +10,13 @@ import { } from '@/api/utils/reactQuery'; import { Logs } from '@/utils/logging'; import { useLogging } from '@/utils/useLogging'; -import { useGroupStore } from '@/zustand/group/stateGroupStore'; import { RealtimeClient, RealtimeEventHandler } from '.'; export function useRealtimeConnection() { - const { groupIds } = useGroupStore(); - const qc = useQueryClient(); - const client = useRef(new RealtimeClient(env.realtimeBaseUrl, groupIds)); + const client = useRef(null); const { writeLog } = useLogging(); @@ -28,7 +25,11 @@ export function useRealtimeConnection() { } const { invalidateLeaderboard } = useQueryInvalidation(); - const hoher: RealtimeEventHandler = (e) => { + const onRealtimeEvent: RealtimeEventHandler = (e) => { + if (!client.current) return; + + console.log('received event'); + switch (e.eventType) { case 'GROUPS': client.current.logger.info('refetching groups'); @@ -190,23 +191,25 @@ export function useRealtimeConnection() { if (client.current) { client.current.logger.addEventListener('*', writeLogs); - client.current.on.event(hoher); + client.current.on.event(onRealtimeEvent); - return () => - client.current.logger.removeEventListener('*', writeLogs); + return () => { + if (client.current) { + client.current.logger.removeEventListener('*', writeLogs); + } + }; } }, [client.current]); - useEffect(() => { - client.current.subscribeToGroups(groupIds); - }, [groupIds]); - function refetchGroup(groupId: string) { qc.invalidateQueries({ queryKey: [QK.group, groupId], exact: true, }); } + function connectRealtime(groupIds: string[]) { + client.current = new RealtimeClient(env.realtimeBaseUrl, groupIds); + } - return client.current; + return { realtime: client.current, connectRealtime }; } diff --git a/mobile-app/api/utils/create-api.tsx b/mobile-app/api/utils/create-api.tsx index c580bbca..37ff1cb3 100644 --- a/mobile-app/api/utils/create-api.tsx +++ b/mobile-app/api/utils/create-api.tsx @@ -12,11 +12,12 @@ import { env } from '@/api/env'; import beerpongDefinition from '@/api/generated/openapi.json'; import { RealtimeClient } from '@/api/realtime'; import { useRealtimeConnection } from '@/api/realtime/useRealtimeConnection'; +import { useAuth } from '@/app/auth/useAuth'; import { Client as BeerPongClient } from '@/openapi/openapi'; import { useLogging } from '@/utils/useLogging'; type ApiContextType = { - realtime: RealtimeClient; + realtime: RealtimeClient | null; api: Promise; isLoading: boolean; error: Error | null; @@ -32,11 +33,19 @@ const openApi = new OpenAPIClientAxios({ }); export function ApiProvider({ children }: { children: ReactNode }) { + const auth = useAuth(); + const api = useRef( new Promise(async (resolve) => { - const awaitedApi = await openApi.getClient(); + const client = await openApi.init(); + + const { accessToken } = await auth.getAccessToken(client); + + client.interceptors.request.use((config) => { + config.headers.Authorization = 'Bearer ' + accessToken; - const client = await openApi.init(); + return config; + }); client.interceptors.response.use( (res) => { @@ -84,11 +93,12 @@ export function ApiProvider({ children }: { children: ReactNode }) { return Promise.reject(err); } ); - resolve(awaitedApi); + // without this, the auth interceptor will not get fired + setTimeout(() => resolve(client), 0); }) ); - const realtime = useRealtimeConnection(); + const { realtime } = useRealtimeConnection(); const { writeLog } = useLogging(); // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/mobile-app/app/(tabs)/_layout.tsx b/mobile-app/app/(tabs)/_layout.tsx index d6bc49c5..37510f7e 100644 --- a/mobile-app/app/(tabs)/_layout.tsx +++ b/mobile-app/app/(tabs)/_layout.tsx @@ -26,8 +26,6 @@ const GroupsButton = () => { }; export default function TabLayout() { - const nav = useNavigation(); - const { selectedGroupId } = useGroupStore(); const selectedGroup = useGroupQuery(selectedGroupId); @@ -39,11 +37,6 @@ export default function TabLayout() { const headerTitleIfGroupIsLoading = ''; const headerTitleIfGroupCantBeFound = ''; - if (!selectedGroupId) { - nav.navigate('onboarding'); - return; - } - const groupHeader = { ...navStyles, headerTitle: selectedGroup.isLoading diff --git a/mobile-app/app/_layout.tsx b/mobile-app/app/_layout.tsx index c8554484..c3a7208e 100644 --- a/mobile-app/app/_layout.tsx +++ b/mobile-app/app/_layout.tsx @@ -16,6 +16,7 @@ import 'react-native-reanimated'; import { RootSiblingParent } from 'react-native-root-siblings'; import { env } from '@/api/env'; +import { useRealtimeConnection } from '@/api/realtime/useRealtimeConnection'; import { ApiProvider } from '@/api/utils/create-api'; import { createQueryClient, persister } from '@/api/utils/query-client'; import { useRefetchEverythingOnWifiReconnect } from '@/api/utils/useRefetchEverythingOnWifiReconnect'; @@ -25,6 +26,7 @@ import { Sidebar } from '@/components/screens/Sidebar'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useTheme } from '@/theme'; import { LoggingProvider } from '@/utils/useLogging'; +import { useGroupStore } from '@/zustand/group/stateGroupStore'; import { ScopePickerProvider } from '@/zustand/useScopePicker'; // https://sentry.io is a error reporting SaaS we use to remotely track production issues @@ -36,6 +38,14 @@ const Drawer = createDrawerNavigator(); SplashScreen.preventAutoHideAsync(); function Everything() { + const { connectRealtime } = useRealtimeConnection(); + + const { groupIds } = useGroupStore(); + + useEffect(() => { + connectRealtime(groupIds); + }, [groupIds]); + const modalStyles = useModalStyles(); return ( diff --git a/mobile-app/app/auth/useAuth.ts b/mobile-app/app/auth/useAuth.ts new file mode 100644 index 00000000..c50ff4ae --- /dev/null +++ b/mobile-app/app/auth/useAuth.ts @@ -0,0 +1,229 @@ +import * as Sentry from '@sentry/react-native'; +import * as Application from 'expo-application'; +import { isAxiosError } from 'axios'; +// import * as Notifications from 'expo-notifications'; +// import * as Permissions from 'expo-permissions'; +import jwt, { JWTBody, JWTDefaultBody } from 'expo-jwt'; +import { useState } from 'react'; +import { Platform } from 'react-native'; + +import { versusDeviceStorage } from '@/app/deviceStorage'; +import { Client as BeerPongClient } from '@/openapi/openapi'; +import { ConsoleLogger } from '@/utils/logging'; + +const REGENERATE_ACCESS_TOKEN_WHEN_ITS_ABOUT_TO_EXPIRE_IN_SECONDS = 10; + +/** + * should be unique for every device, even across reinstalls + */ +async function getInstallationId() { + // important: the platform-specific Application methods throw an error if they're called on the wrong platform! + const installationId = + Platform.OS === 'ios' + ? await Application.getIosIdForVendorAsync() + : Application.getAndroidId(); + + if (installationId == null) { + throw new Error('failed to get installation id'); + } + return installationId; +} + +// async function registerForPushNotifications() { +// const { status: existingStatus } = await Permissions.getAsync( +// Permissions.NOTIFICATIONS +// ); +// let finalStatus = existingStatus; + +// if (existingStatus !== 'granted') { +// const { status } = await Permissions.askAsync( +// Permissions.NOTIFICATIONS +// ); +// finalStatus = status; +// } +// if (finalStatus !== 'granted') return null; + +// const tokenData = await Notifications.getExpoPushTokenAsync(); + +// const pushNotificationToken = tokenData.data; // e.g. “ExponentPushToken[xxxxxxxxxxxxxx]” +// } + +// export function usePushNotifications() { +// const registerForPushNotificationsMutation = +// useRegisterForPushNotificationsMutation(); + +// return { +// registerForPushNotifications: async function () { +// const expoToken = await registerForPushNotifications(); +// if (expoToken) { +// await registerForPushNotificationsMutation.mutateAsync( +// expoToken +// ); +// } +// }, +// }; +// } + +async function getRefreshToken(api: BeerPongClient): Promise { + try { + const existingRefreshToken = + await versusDeviceStorage.getRefreshToken(); + + if (existingRefreshToken) return existingRefreshToken; + + const installationType = Platform.OS === 'ios' ? 'IOS' : 'ANDROID'; + + const deviceId = await getInstallationId(); + + const signupRes = await api.signup(undefined, { + installationType, + deviceId, + }); + + const refreshToken = signupRes.data.data?.token; + + if (!refreshToken) { + throw new Error('response.data.data.token is null'); + } + await versusDeviceStorage.setRefreshToken(refreshToken); + + return refreshToken; + } catch (err) { + ConsoleLogger.error('Failed to get refresh token:', err); + + (err as Error).message = + 'Failed to get refresh token: ' + + (err instanceof Error ? err.message : 'Unknown error'); + + throw err; + } +} + +interface GetAccessTokenResult { + accessToken: string; + accessTokenPayload: JWTBody; +} + +async function getAccessToken( + api: BeerPongClient +): Promise { + let refreshToken: string | null = null; + try { + refreshToken = await getRefreshToken(api); + + const accessTokenRes = await api.refreshAuth(undefined, { + refreshToken, + }); + + const accessToken = accessTokenRes.data.data?.token; + + if (!accessToken) { + throw new Error('response.data.data.token is null'); + } + try { + const accessTokenPayload = jwt.decode(accessToken, null); + + return { accessToken, accessTokenPayload }; + } catch (err) { + throw new Error( + 'Failed to decode: ' + + (err instanceof Error ? err.message : 'Unknown error') + ); + } + } catch (err) { + ConsoleLogger.error( + 'Failed to get access token:', + err, + 'with refreshToken', + refreshToken + ); + if (isAxiosError(err)) { + // more detailed error response returned by the backend, can be found in ErrorCodes.java + const customErrorCode = err.response?.data.error?.code; + + // standard http error code, e.g. "Bad Request", automatically thrown by spring boot + const httpErrorCode = err.response?.data.error; + + const isInvalidRefreshToken = + customErrorCode === 'authRefreshInvalidToken'; + + if (isInvalidRefreshToken) { + ConsoleLogger.error( + 'Failed to get access token: refresh token not accepted by backend' + ); + await versusDeviceStorage.removeRefreshToken(); + + err = new Error( + 'Failed to get access token: refresh token not accepted by backend' + ); + } else { + const message = customErrorCode ?? httpErrorCode ?? err.message; + + (err as Error).message = + 'Failed to get access token: ' + + message + + ': ' + + err.message; + } + } else { + (err as Error).message = + 'Failed to get access token: ' + + ((err as Error).message ?? 'Unknown error'); + } + + Sentry.captureException(err, { + extra: { + refreshToken, + installationId: getInstallationId(), + }, + }); + throw err; + } +} + +export function useAuth() { + const [fetchingPromise, setFetchingPromise] = + useState | null>(null); + const [accessToken, setAccessToken] = useState( + null + ); + + return { + getAccessToken: async (api: BeerPongClient) => { + const isAboutToExpire = + (accessToken?.accessTokenPayload.exp ?? 0) < + Date.now() - + REGENERATE_ACCESS_TOKEN_WHEN_ITS_ABOUT_TO_EXPIRE_IN_SECONDS * + 1000; + + if (accessToken && !isAboutToExpire) { + return accessToken; + } + if (fetchingPromise) return await fetchingPromise; + + try { + ConsoleLogger.info('refreshing access token'); + + const promise = getAccessToken(api); + + setFetchingPromise(promise); + + const value = await promise; + + setAccessToken(value); + + return value; + } catch (err) { + ConsoleLogger.error( + 'Failed to resolve access token promise:', + err + ); + setFetchingPromise(null); + (err as Error).message = + 'Failed to retrieve access token: ' + + (err instanceof Error ? err.message : 'Unknown error'); + throw err; + } + }, + }; +} diff --git a/mobile-app/app/createGroupSetGame.tsx b/mobile-app/app/createGroupSetGame.tsx index cf0827e8..1d5db12b 100644 --- a/mobile-app/app/createGroupSetGame.tsx +++ b/mobile-app/app/createGroupSetGame.tsx @@ -1,3 +1,4 @@ +import { useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'expo-router'; import React from 'react'; @@ -5,21 +6,21 @@ import { useCreateGroupMutation, useGroupPresetsQuery, } from '@/api/calls/groupHooks'; +import { QK } from '@/api/utils/reactQuery'; import LoadingScreen from '@/components/LoadingScreen'; import { CreateGroupSetGame } from '@/components/screens/CreateGroupSetGame'; import { showErrorToast, showSuccessToast } from '@/toast'; import { ConsoleLogger } from '@/utils/logging'; import { useCreateGroupStore } from '@/zustand/group/stateCreateGroupStore'; -import { useGroupStore } from '@/zustand/group/stateGroupStore'; import { useMatchDraftStore } from '@/zustand/matchDraftStore'; export default function Page() { const router = useRouter(); const { members, name } = useCreateGroupStore(); const createGroupMutation = useCreateGroupMutation(); - const { addGroup } = useGroupStore(); const presetsQuery = useGroupPresetsQuery(); const matchDraft = useMatchDraftStore(); + const queryClient = useQueryClient(); const presets = presetsQuery.data?.data?.map((i) => ({ @@ -46,8 +47,9 @@ export default function Page() { if (!data?.data?.id) { throw new Error('invalid create group response'); } - addGroup(data.data.id); - + await queryClient.invalidateQueries({ + queryKey: [QK.group, 'myGroups'], + }); matchDraft.actions.clear(); showSuccessToast(`You created "${name}"`); diff --git a/mobile-app/app/debugLog.tsx b/mobile-app/app/debugLog.tsx index 1b72b75a..b50bb1bf 100644 --- a/mobile-app/app/debugLog.tsx +++ b/mobile-app/app/debugLog.tsx @@ -41,8 +41,8 @@ export default function Page() { useEffect(() => { // we need to keep this in state because `realtime` is a ref and will not cause a rerender if it changes, // so the indicator could be misleading - setIsRealtimeOpen(realtime.isOpen); - }, [realtime.isOpen]); + setIsRealtimeOpen(realtime?.isOpen ?? false); + }, [realtime?.isOpen]); const theme = useTheme(); diff --git a/mobile-app/app/deviceStorage.ts b/mobile-app/app/deviceStorage.ts new file mode 100644 index 00000000..21ada225 --- /dev/null +++ b/mobile-app/app/deviceStorage.ts @@ -0,0 +1,58 @@ +import * as SecureStore from 'expo-secure-store'; + +import { ScopedLogger } from '@/utils/logging'; + +class VersusDeviceStorage { + private store = SecureStore; + + private logger = new ScopedLogger('versus-device-storage'); + + private KEYCHAIN_SERVICE = 'Versus'; + + private REFRESH_TOKEN_KEY = 'refresh-token'; + + private async getItemAsync(key: string) { + try { + return this.store.getItemAsync(key, { + keychainAccessible: 0, + keychainService: this.KEYCHAIN_SERVICE, + }); + } catch (err) { + this.logger.error(`Failed to get item with key "${key}":`, err); + } + } + private async setItemAsync(key: string, value: string) { + try { + return this.store.setItemAsync(key, value, { + keychainAccessible: 0, + keychainService: this.KEYCHAIN_SERVICE, + }); + } catch (err) { + this.logger.error( + `Failed to set item with key "${key}": to value "${value}"`, + err + ); + } + } + private async removeItemAsync(key: string) { + try { + return this.store.deleteItemAsync(key, { + keychainAccessible: 0, + keychainService: this.KEYCHAIN_SERVICE, + }); + } catch (err) { + this.logger.error(`Failed to remove item with key "${key}":`, err); + } + } + + public async getRefreshToken() { + return this.getItemAsync(this.REFRESH_TOKEN_KEY); + } + public async setRefreshToken(token: string) { + return this.setItemAsync(this.REFRESH_TOKEN_KEY, token); + } + public async removeRefreshToken() { + return this.removeItemAsync(this.REFRESH_TOKEN_KEY); + } +} +export const versusDeviceStorage = new VersusDeviceStorage(); diff --git a/mobile-app/app/joinGroup.tsx b/mobile-app/app/joinGroup.tsx index 4669b0e5..f6f3b5f7 100644 --- a/mobile-app/app/joinGroup.tsx +++ b/mobile-app/app/joinGroup.tsx @@ -1,8 +1,10 @@ +import { useQueryClient } from '@tanstack/react-query'; import { AxiosError } from 'axios'; import { useRouter } from 'expo-router'; import React from 'react'; -import { useJoinGroupMutation } from '@/api/calls/groupHooks'; +import { QK } from '@/api/utils/reactQuery'; +import { useNavigation } from '@/app/navigation/useNavigation'; import JoinGroup from '@/components/screens/JoinGroup'; import { showSuccessToast } from '@/toast'; import { ConsoleLogger } from '@/utils/logging'; @@ -10,18 +12,26 @@ import { useGroupStore } from '@/zustand/group/stateGroupStore'; export default function Page() { const router = useRouter(); - const joinGroupMutation = useJoinGroupMutation(); - const { addGroup, selectGroup } = useGroupStore(); + const nav = useNavigation(); + + const { joinGroupMutation, selectGroup } = useGroupStore(); + + const queryClient = useQueryClient(); async function onSubmit(code: string) { try { const data = await joinGroupMutation.mutateAsync(code); if (data?.data?.id) { - addGroup(data.data.id); selectGroup(data.data.id); + await queryClient.invalidateQueries({ + queryKey: [QK.group, 'myGroups'], + }); + + nav.navigate('index'); + showSuccessToast(`You joined "${data.data.name}"`); router.dismissAll(); router.replace('/'); diff --git a/mobile-app/components/OnboardingModal.tsx b/mobile-app/components/OnboardingModal.tsx index 9a26ee24..a66ed4c9 100644 --- a/mobile-app/components/OnboardingModal.tsx +++ b/mobile-app/components/OnboardingModal.tsx @@ -43,7 +43,7 @@ export default function OnboardingModal() { Welcome to{' '} { + onPress: async () => { if (groupIdToBeDeleted) { - removeGroup(groupIdToBeDeleted); + await leaveGroupMutation.mutateAsync( + groupIdToBeDeleted + ); + await queryClient.invalidateQueries({ + queryKey: [QK.group, 'myGroups'], + }); } setGroupIdToBeDeleted(null); }, diff --git a/mobile-app/openapi/openapi.d.ts b/mobile-app/openapi/openapi.d.ts index 981d74c3..46d1595f 100644 --- a/mobile-app/openapi/openapi.d.ts +++ b/mobile-app/openapi/openapi.d.ts @@ -21,6 +21,17 @@ declare namespace Components { offsetY?: number; // double zoom?: number; // double } + export interface AuthRefreshDto { + refreshToken?: string; + } + export interface AuthSignupDto { + installationType?: 'IOS' | 'ANDROID'; + deviceId?: string; + } + export interface AuthTokenDto { + token?: string; + type?: 'ACCESS' | 'REFRESH'; + } export interface ErrorDetails { code?: string; description?: string; @@ -35,8 +46,9 @@ declare namespace Components { id?: string; name?: string; inviteCode?: string; - activeSeason?: Season; + activeSeason?: SeasonDto; wallpaperAsset?: AssetMetadataDto; + createdBy?: GroupMemberDto; createdAt?: string; // date-time sportPreset?: GroupPreset; customSportName?: string; @@ -44,6 +56,12 @@ declare namespace Components { numberOfMatches?: number; // int32 numberOfSeasons?: number; // int32 } + export interface GroupMemberDto { + id?: string; + active?: boolean; + groupId?: string; + userId?: string; + } export interface GroupPreset { id?: string; title?: string; @@ -55,13 +73,20 @@ declare namespace Components { startedAt?: string; // date-time entries?: PlayerDto[]; } + export interface LocalTime { + hour?: number; // int32 + minute?: number; // int32 + second?: number; // int32 + nano?: number; // int32 + } export interface MatchCreateDto { teams?: TeamCreateDto[]; } export interface MatchDto { id?: string; date?: string; // date-time - season?: Season; + season?: SeasonDto; + createdBy?: GroupMemberDto; teams?: TeamDto[]; teamMembers?: TeamMemberDto[]; matchMoves?: MatchMoveDtoComplete[]; @@ -79,7 +104,7 @@ declare namespace Components { export interface MatchOverviewDto { id?: string; date?: string; // date-time - season?: Season; + season?: SeasonDto; blueTeam?: MatchOverviewTeamDto; redTeam?: MatchOverviewTeamDto; } @@ -122,6 +147,7 @@ declare namespace Components { name?: string; avatarAsset?: AssetMetadataDto; groupId?: string; + createdBy?: GroupMemberDto; reactivated?: boolean; lastActiveSeasonId?: string; } @@ -130,6 +156,7 @@ declare namespace Components { name?: string; avatarAsset?: AssetMetadataDto; groupId?: string; + createdBy?: GroupMemberDto; } export interface ResponseEnvelopeAssetMetadataDto { status?: 'OK' | 'ERROR'; @@ -137,6 +164,12 @@ declare namespace Components { data?: AssetMetadataDto; error?: ErrorDetails; } + export interface ResponseEnvelopeAuthTokenDto { + status?: 'OK' | 'ERROR'; + httpCode?: number; // int32 + data?: AuthTokenDto; + error?: ErrorDetails; + } export interface ResponseEnvelopeGroupDto { status?: 'OK' | 'ERROR'; httpCode?: number; // int32 @@ -149,6 +182,12 @@ declare namespace Components { data?: LeaderboardDto; error?: ErrorDetails; } + export interface ResponseEnvelopeListGroupDto { + status?: 'OK' | 'ERROR'; + httpCode?: number; // int32 + data?: GroupDto[]; + error?: ErrorDetails; + } export interface ResponseEnvelopeListGroupPreset { status?: 'OK' | 'ERROR'; httpCode?: number; // int32 @@ -253,7 +292,8 @@ declare namespace Components { id?: string; title?: string; description?: string; - season?: Season; + season?: SeasonDto; + createdBy?: GroupMemberDto; } export interface RuleMoveCreateDto { name?: string; @@ -267,15 +307,7 @@ declare namespace Components { pointsForTeam?: number; // int32 pointsForScorer?: number; // int32 finishingMove?: boolean; - season?: Season; - } - export interface Season { - id?: string; - name?: string; - startDate?: string; // date-time - endDate?: string; // date-time - groupId?: string; - seasonSettings?: SeasonSettings; + season?: SeasonDto; } export interface SeasonCreateDto { oldSeasonName?: string; @@ -288,6 +320,7 @@ declare namespace Components { endDate?: string; // date-time groupId?: string; seasonSettings?: SeasonSettings; + createdBy?: GroupMemberDto; } export interface SeasonSettings { id?: string; @@ -299,10 +332,21 @@ declare namespace Components { | 'RESET_AT_MIDNIGHT' | 'WAKE_TIME' | 'LAST_24_HOURS'; - wakeTimeHour?: number; // int32 + wakeTime?: LocalTime; + } + export interface SeasonSettingsDto { + minMatchesToQualify?: number; // int32 + minTeamSize?: number; // int32 + maxTeamSize?: number; // int32 + rankingAlgorithm?: 'AVERAGE' | 'ELO'; + dailyLeaderboard?: + | 'RESET_AT_MIDNIGHT' + | 'WAKE_TIME' + | 'LAST_24_HOURS'; + wakeTime?: string; } export interface SeasonUpdateDto { - seasonSettings: SeasonSettings; + seasonSettings: SeasonSettingsDto; } export interface TeamCreateDto { existingTeamId?: string; @@ -410,9 +454,9 @@ declare namespace Paths { } export interface PathParameters { groupId: Parameters.GroupId; - seasonId: Parameters.SeasonId; - id: Parameters.Id; teamId: Parameters.TeamId; + id: Parameters.Id; + seasonId: Parameters.SeasonId; } namespace Responses { export type $200 = Components.Schemas.ResponseEnvelopeTeamDto; @@ -455,6 +499,11 @@ declare namespace Paths { export type $200 = Components.Schemas.ResponseEnvelopeGroupDto; } } + namespace FindUserGroups { + namespace Responses { + export type $200 = Components.Schemas.ResponseEnvelopeListGroupDto; + } + } namespace GetAllMatchOverviews { namespace Parameters { export type GroupId = string; @@ -648,6 +697,28 @@ declare namespace Paths { export type $200 = Components.Schemas.ResponseEnvelopeSeasonDto; } } + namespace JoinGroup { + namespace Parameters { + export type Id = string; + } + export interface PathParameters { + id: Parameters.Id; + } + namespace Responses { + export type $200 = Components.Schemas.ResponseEnvelopeString; + } + } + namespace LeaveGroup { + namespace Parameters { + export type Id = string; + } + export interface PathParameters { + id: Parameters.Id; + } + namespace Responses { + export type $200 = Components.Schemas.ResponseEnvelopeString; + } + } namespace ListAllProfiles { namespace Parameters { export type GroupId = string; @@ -660,6 +731,12 @@ declare namespace Paths { Components.Schemas.ResponseEnvelopeListProfileDto; } } + namespace RefreshAuth { + export type RequestBody = Components.Schemas.AuthRefreshDto; + namespace Responses { + export type $200 = Components.Schemas.ResponseEnvelopeAuthTokenDto; + } + } namespace SetAvatar { namespace Parameters { export type GroupId = string; @@ -683,9 +760,9 @@ declare namespace Paths { } export interface PathParameters { groupId: Parameters.GroupId; - seasonId: Parameters.SeasonId; - id: Parameters.Id; teamId: Parameters.TeamId; + id: Parameters.Id; + seasonId: Parameters.SeasonId; } namespace Responses { export type $200 = Components.Schemas.ResponseEnvelopeTeamDto; @@ -704,6 +781,12 @@ declare namespace Paths { Components.Schemas.ResponseEnvelopeAssetMetadataDto; } } + namespace Signup { + export type RequestBody = Components.Schemas.AuthSignupDto; + namespace Responses { + export type $200 = Components.Schemas.ResponseEnvelopeAuthTokenDto; + } + } namespace StartNewSeason { namespace Parameters { export type GroupId = string; @@ -973,6 +1056,22 @@ export interface OperationMethods { data?: Paths.CreateGroup.RequestBody, config?: AxiosRequestConfig ): OperationResponse; + /** + * leaveGroup + */ + 'leaveGroup'( + parameters?: Parameters | null, + data?: any, + config?: AxiosRequestConfig + ): OperationResponse; + /** + * joinGroup + */ + 'joinGroup'( + parameters?: Parameters | null, + data?: any, + config?: AxiosRequestConfig + ): OperationResponse; /** * getAllRuleMoves */ @@ -1021,6 +1120,22 @@ export interface OperationMethods { data?: Paths.CreateProfile.RequestBody, config?: AxiosRequestConfig ): OperationResponse; + /** + * signup + */ + 'signup'( + parameters?: Parameters | null, + data?: Paths.Signup.RequestBody, + config?: AxiosRequestConfig + ): OperationResponse; + /** + * refreshAuth + */ + 'refreshAuth'( + parameters?: Parameters | null, + data?: Paths.RefreshAuth.RequestBody, + config?: AxiosRequestConfig + ): OperationResponse; /** * getHealthcheck */ @@ -1074,6 +1189,14 @@ export interface OperationMethods { data?: any, config?: AxiosRequestConfig ): OperationResponse; + /** + * findUserGroups + */ + 'findUserGroups'( + parameters?: Parameters | null, + data?: any, + config?: AxiosRequestConfig + ): OperationResponse; /** * getPresets */ @@ -1291,6 +1414,26 @@ export interface PathsDictionary { config?: AxiosRequestConfig ): OperationResponse; }; + ['/groups/{id}/leave']: { + /** + * leaveGroup + */ + 'post'( + parameters?: Parameters | null, + data?: any, + config?: AxiosRequestConfig + ): OperationResponse; + }; + ['/groups/{id}/join']: { + /** + * joinGroup + */ + 'post'( + parameters?: Parameters | null, + data?: any, + config?: AxiosRequestConfig + ): OperationResponse; + }; ['/groups/{groupId}/seasons/{seasonId}/rule-moves']: { /** * getAllRuleMoves @@ -1345,6 +1488,26 @@ export interface PathsDictionary { config?: AxiosRequestConfig ): OperationResponse; }; + ['/auth/signup']: { + /** + * signup + */ + 'post'( + parameters?: Parameters | null, + data?: Paths.Signup.RequestBody, + config?: AxiosRequestConfig + ): OperationResponse; + }; + ['/auth/refresh']: { + /** + * refreshAuth + */ + 'post'( + parameters?: Parameters | null, + data?: Paths.RefreshAuth.RequestBody, + config?: AxiosRequestConfig + ): OperationResponse; + }; ['/healthcheck']: { /** * getHealthcheck @@ -1411,6 +1574,16 @@ export interface PathsDictionary { config?: AxiosRequestConfig ): OperationResponse; }; + ['/groups/user']: { + /** + * findUserGroups + */ + 'get'( + parameters?: Parameters | null, + data?: any, + config?: AxiosRequestConfig + ): OperationResponse; + }; ['/group-presets']: { /** * getPresets @@ -1447,11 +1620,16 @@ export type Client = OpenAPIClient; export type AssetCropDto = Components.Schemas.AssetCropDto; export type AssetMetadataDto = Components.Schemas.AssetMetadataDto; +export type AuthRefreshDto = Components.Schemas.AuthRefreshDto; +export type AuthSignupDto = Components.Schemas.AuthSignupDto; +export type AuthTokenDto = Components.Schemas.AuthTokenDto; export type ErrorDetails = Components.Schemas.ErrorDetails; export type GroupCreateDto = Components.Schemas.GroupCreateDto; export type GroupDto = Components.Schemas.GroupDto; +export type GroupMemberDto = Components.Schemas.GroupMemberDto; export type GroupPreset = Components.Schemas.GroupPreset; export type LeaderboardDto = Components.Schemas.LeaderboardDto; +export type LocalTime = Components.Schemas.LocalTime; export type MatchCreateDto = Components.Schemas.MatchCreateDto; export type MatchDto = Components.Schemas.MatchDto; export type MatchMoveDto = Components.Schemas.MatchMoveDto; @@ -1467,10 +1645,14 @@ export type ProfileCreatedDto = Components.Schemas.ProfileCreatedDto; export type ProfileDto = Components.Schemas.ProfileDto; export type ResponseEnvelopeAssetMetadataDto = Components.Schemas.ResponseEnvelopeAssetMetadataDto; +export type ResponseEnvelopeAuthTokenDto = + Components.Schemas.ResponseEnvelopeAuthTokenDto; export type ResponseEnvelopeGroupDto = Components.Schemas.ResponseEnvelopeGroupDto; export type ResponseEnvelopeLeaderboardDto = Components.Schemas.ResponseEnvelopeLeaderboardDto; +export type ResponseEnvelopeListGroupDto = + Components.Schemas.ResponseEnvelopeListGroupDto; export type ResponseEnvelopeListGroupPreset = Components.Schemas.ResponseEnvelopeListGroupPreset; export type ResponseEnvelopeListMatchDto = @@ -1506,10 +1688,10 @@ export type RuleCreateDto = Components.Schemas.RuleCreateDto; export type RuleDto = Components.Schemas.RuleDto; export type RuleMoveCreateDto = Components.Schemas.RuleMoveCreateDto; export type RuleMoveDto = Components.Schemas.RuleMoveDto; -export type Season = Components.Schemas.Season; export type SeasonCreateDto = Components.Schemas.SeasonCreateDto; export type SeasonDto = Components.Schemas.SeasonDto; export type SeasonSettings = Components.Schemas.SeasonSettings; +export type SeasonSettingsDto = Components.Schemas.SeasonSettingsDto; export type SeasonUpdateDto = Components.Schemas.SeasonUpdateDto; export type TeamCreateDto = Components.Schemas.TeamCreateDto; export type TeamDto = Components.Schemas.TeamDto; diff --git a/mobile-app/zustand/group/stateGroupStore.ts b/mobile-app/zustand/group/stateGroupStore.ts index 3166699d..d06879b5 100644 --- a/mobile-app/zustand/group/stateGroupStore.ts +++ b/mobile-app/zustand/group/stateGroupStore.ts @@ -1,113 +1,63 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; +import { useEffect, useMemo } from 'react'; import { create } from 'zustand'; -import { createJSONStorage, persist } from 'zustand/middleware'; -interface GroupState { - // State - groupIds: string[]; - selectedGroupId: string | null; - isLoading: boolean; - error: string | null; - lastUpdated: number | null; - - // Basic CRUD - addGroup: (groupId: string) => void; - removeGroup: (groupId: string) => void; - clearGroups: () => void; +import { + useGetMyGroupsQuery, + useJoinGroupMutation, + useLeaveGroupMutation, +} from '@/api/calls/groupHooks'; +import { useNavigation } from '@/app/navigation/useNavigation'; - // Selection +const useStore = create<{ + selectedGroupId: string | null; selectGroup: (groupId: string | null) => void; - getSelectedGroupId: () => string | null; +}>()((set) => ({ + selectedGroupId: null, - // Utility - setError: (error: string | null) => void; - setLoading: (isLoading: boolean) => void; -} + selectGroup: (selectedGroupId) => { + set({ selectedGroupId }); + }, +})); -export const useGroupStore = create()( - persist( - (set, get) => ({ - // Initial state - groupIds: [], - selectedGroupId: null, - isLoading: false, - error: null, - lastUpdated: null, +export function useGroupStore() { + const store = useStore(); - // Initialize the store - initialize: async () => { - set({ isLoading: false, error: null }); - }, + const myGroupsQuery = useGetMyGroupsQuery(); - // Add a new group - addGroup: (groupId) => { - set((state) => ({ - groupIds: [...new Set([...state.groupIds, groupId])], - selectedGroupId: groupId, - lastUpdated: Date.now(), - error: null, - })); - }, + const leaveGroupMutation = useLeaveGroupMutation(); - // Remove a group - removeGroup: (groupId) => { - set((state) => ({ - groupIds: state.groupIds.filter( - (group) => group !== groupId - ), - selectedGroupId: - state.selectedGroupId === groupId - ? (state.groupIds.find((i) => i !== groupId) ?? - null) - : state.selectedGroupId, - lastUpdated: Date.now(), - error: null, - })); - }, + const joinGroupMutation = useJoinGroupMutation(); - // Clear all groups - clearGroups: () => { - set({ - groupIds: [], - selectedGroupId: null, - lastUpdated: Date.now(), - error: null, - }); - }, + const groupIds = useMemo( + () => myGroupsQuery.data?.data?.map((g) => g.id!) ?? [], + [myGroupsQuery.data] + ); + const isLoadingGroups = myGroupsQuery.isLoading; - // Select a group - selectGroup: (groupId) => { - if ( - groupId === null || - get().groupIds.some((g) => g === groupId) - ) { - set({ selectedGroupId: groupId, error: null }); - } else { - set({ error: 'Invalid group selection' }); - } - }, + const nav = useNavigation(); - // Get selected group - getSelectedGroupId: () => { - const state = get(); - return ( - state.groupIds.find((g) => g === state.selectedGroupId) || - null - ); - }, + // if this changes we either left, created, or joined a group. + const groupsKey = myGroupsQuery.data?.data?.map((i) => i.id)?.join(); - // Utility functions - setError: (error) => set({ error }), - setLoading: (isLoading) => set({ isLoading }), - }), - { - name: 'group-storage', // name of the item in storage - storage: createJSONStorage(() => AsyncStorage), // AsyncStorage for React Native - partialize: (state) => ({ - // Only persist these fields - groupIds: state.groupIds, - selectedGroupId: state.selectedGroupId, - }), + useEffect(() => { + if (!store.selectedGroupId && myGroupsQuery.data) { + const groupId = myGroupsQuery.data.data?.[0]?.id ?? null; + if (groupId) { + store.selectGroup(myGroupsQuery.data.data?.[0]?.id ?? null); + } else { + nav.navigate('onboarding'); + } } - ) -); + }, [groupsKey]); + + return { + groupIds, + selectedGroupId: store.selectedGroupId, + selectGroup: store.selectGroup, + + myGroupsQuery, + joinGroupMutation, + leaveGroupMutation, + isLoadingGroups, + }; +}