diff --git a/build.gradle b/build.gradle index 2251581..51def51 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,9 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-impl:0.12.6' implementation 'io.jsonwebtoken:jjwt-jackson:0.12.6' + // cache + implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' + // test testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.testcontainers:junit-jupiter:1.20.1' @@ -51,6 +54,8 @@ dependencies { testImplementation 'org.wiremock.integrations.testcontainers:wiremock-testcontainers-module:1.0-alpha-14' testImplementation 'org.testcontainers:postgresql:1.20.3' testImplementation 'org.liquibase:liquibase-core:4.29.2' + testImplementation 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' } tasks.named('test') { diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index b690f74..13c7990 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -56,12 +56,12 @@ - - - - - - + + + + + + @@ -277,12 +277,12 @@ - - - - - + + + + + + diff --git a/src/main/java/com/cf/cfteam/advicers/codeforces/CodeforcesExceptionHandler.java b/src/main/java/com/cf/cfteam/advicers/codeforces/CodeforcesExceptionHandler.java new file mode 100644 index 0000000..07a59de --- /dev/null +++ b/src/main/java/com/cf/cfteam/advicers/codeforces/CodeforcesExceptionHandler.java @@ -0,0 +1,57 @@ +package com.cf.cfteam.advicers.codeforces; + +import com.cf.cfteam.exceptions.codeforces.*; +import com.cf.cfteam.utils.ErrorResponseBuilder; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.util.HashMap; +import java.util.Map; + +@ControllerAdvice +public class CodeforcesExceptionHandler { + private static final String ID = "id"; + private static final String LOGIN = "login"; + private static final String TEAM_ID = "teamId"; + private static final String PLAYER_ID = "playerId"; + + @ExceptionHandler(GroupNotFoundException.class) + public ResponseEntity handleUserNotFoundException(GroupNotFoundException ex) { + return ErrorResponseBuilder.buildErrorResponse(ex.getMessage(), HttpStatus.NOT_FOUND, Map.of(ID, ex.getId())); + } + + @ExceptionHandler(TeamNotFoundException.class) + public ResponseEntity handleTeamNotFoundException(TeamNotFoundException ex) { + return ErrorResponseBuilder.buildErrorResponse(ex.getMessage(), HttpStatus.NOT_FOUND, Map.of(ID, ex.getId())); + } + + @ExceptionHandler(PlayerNotFoundException.class) + public ResponseEntity handlePayerNotFoundException(PlayerNotFoundException ex) { + return ErrorResponseBuilder.buildErrorResponse(ex.getMessage(), HttpStatus.NOT_FOUND, Map.of(ID, ex.getId())); + } + + @ExceptionHandler(PlayerAlreadyInTeamException.class) + public ResponseEntity handlePlayerAlreadyInTeamException(PlayerAlreadyInTeamException ex) { + return ErrorResponseBuilder.buildErrorResponse(ex.getMessage(), HttpStatus.CONFLICT, + Map.of(LOGIN, ex.getLogin())); + } + + @ExceptionHandler(PlayerNotFromTeamException.class) + public ResponseEntity handlePlayerNotFromTeamException(PlayerNotFromTeamException ex) { + Map details = new HashMap<>(); + if (ex.getPlayerId() != null) { + details.put(PLAYER_ID, ex.getPlayerId()); + } + if (ex.getTeamId() != null) { + details.put(TEAM_ID, ex.getTeamId()); + } + + return ErrorResponseBuilder.buildErrorResponse( + ex.getMessage(), + HttpStatus.CONFLICT, + details + ); + } +} diff --git a/src/main/java/com/cf/cfteam/advicers/security/SecurityExceptionHandler.java b/src/main/java/com/cf/cfteam/advicers/security/SecurityExceptionHandler.java index 256a2fc..5cafe3d 100644 --- a/src/main/java/com/cf/cfteam/advicers/security/SecurityExceptionHandler.java +++ b/src/main/java/com/cf/cfteam/advicers/security/SecurityExceptionHandler.java @@ -4,12 +4,12 @@ import com.cf.cfteam.exceptions.security.TokenRevokedException; import com.cf.cfteam.exceptions.security.UserAlreadyRegisterException; import com.cf.cfteam.exceptions.security.UserNotFoundException; +import com.cf.cfteam.utils.ErrorResponseBuilder; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; -import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; @@ -18,44 +18,39 @@ public class SecurityExceptionHandler { private static final String LOGIN = "login"; private static final String TOKEN = "token"; - private static final String UNEXPECTED_ERROR = "unexpected.error"; + private static final String ID = "id"; @ExceptionHandler(UserAlreadyRegisterException.class) public ResponseEntity handleUserAlreadyRegisterException(UserAlreadyRegisterException ex) { - return buildErrorResponse(ex.getMessage(), HttpStatus.CONFLICT, Map.of(LOGIN, ex.getLogin())); + return ErrorResponseBuilder.buildErrorResponse(ex.getMessage(), HttpStatus.CONFLICT, Map.of(LOGIN, + ex.getLogin())); } @ExceptionHandler(UserNotFoundException.class) public ResponseEntity handleUserNotFoundException(UserNotFoundException ex) { - return buildErrorResponse(ex.getMessage(), HttpStatus.NOT_FOUND, Map.of(LOGIN, ex.getLogin())); + Map details = new HashMap<>(); + if (ex.getLogin() != null) { + details.put(LOGIN, ex.getLogin()); + } + if (ex.getId() != null) { + details.put(ID, ex.getId()); + } + + return ErrorResponseBuilder.buildErrorResponse( + ex.getMessage(), + HttpStatus.NOT_FOUND, + details + ); } @ExceptionHandler(TokenRevokedException.class) public ResponseEntity handleTokenRevokedException(TokenRevokedException ex) { - return buildErrorResponse(ex.getMessage(), HttpStatus.UNAUTHORIZED, Map.of(TOKEN, ex.getToken())); + return ErrorResponseBuilder.buildErrorResponse(ex.getMessage(), HttpStatus.UNAUTHORIZED, Map.of(TOKEN, + ex.getToken())); } @ExceptionHandler(InvalidTwoFactorCodeException.class) public ResponseEntity handleInvalidTwoFactorCodeException(InvalidTwoFactorCodeException ex) { - return buildErrorResponse(ex.getMessage(), HttpStatus.BAD_REQUEST, null); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity handleGenericException(Exception ex) { - return buildErrorResponse(UNEXPECTED_ERROR, HttpStatus.INTERNAL_SERVER_ERROR, null); - } - - private ResponseEntity buildErrorResponse(String message, HttpStatus status, Map details) { - Map errorResponse = new HashMap<>(); - errorResponse.put("timestamp", LocalDateTime.now()); - errorResponse.put("status", status.value()); - errorResponse.put("error", status.getReasonPhrase()); - errorResponse.put("message", message); - - if (details != null) { - errorResponse.put("details", details); - } - - return new ResponseEntity<>(errorResponse, status); + return ErrorResponseBuilder.buildErrorResponse(ex.getMessage(), HttpStatus.BAD_REQUEST, null); } } diff --git a/src/main/java/com/cf/cfteam/config/AppConfig.java b/src/main/java/com/cf/cfteam/config/AppConfig.java index 9f1750e..4c2d356 100644 --- a/src/main/java/com/cf/cfteam/config/AppConfig.java +++ b/src/main/java/com/cf/cfteam/config/AppConfig.java @@ -2,8 +2,11 @@ import com.cf.cfteam.services.security.MyUserDetailsService; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; @@ -11,6 +14,7 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.client.RestClient; @Configuration @@ -41,4 +45,12 @@ public UserDetailsService userDetailsService() { public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + @Bean + public RestClient restClient(@Value("${codeforces.api.base.url}") String url) { + return RestClient.builder() + .baseUrl(url) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .build(); + } } diff --git a/src/main/java/com/cf/cfteam/config/CacheConfig.java b/src/main/java/com/cf/cfteam/config/CacheConfig.java new file mode 100644 index 0000000..5cacf6f --- /dev/null +++ b/src/main/java/com/cf/cfteam/config/CacheConfig.java @@ -0,0 +1,25 @@ +package com.cf.cfteam.config; + +import com.cf.cfteam.services.client.CodeforcesClient; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.TimeUnit; + +@Configuration +@RequiredArgsConstructor +public class CacheConfig { + + private final CodeforcesClient codeforcesClient; + + @Bean + public LoadingCache ratingCache() { + return Caffeine.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .maximumSize(1000) + .build(codeforcesClient::fetchRatingFromApi); + } +} diff --git a/src/main/java/com/cf/cfteam/controllers/codeforces/GroupController.java b/src/main/java/com/cf/cfteam/controllers/codeforces/GroupController.java new file mode 100644 index 0000000..384a5fd --- /dev/null +++ b/src/main/java/com/cf/cfteam/controllers/codeforces/GroupController.java @@ -0,0 +1,59 @@ +package com.cf.cfteam.controllers.codeforces; + +import com.cf.cfteam.services.codeforces.GroupService; +import com.cf.cfteam.transfer.payloads.codeforces.GroupPayload; +import com.cf.cfteam.transfer.responses.codeforces.GroupResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("api/cf/groups") +public class GroupController { + + private final GroupService groupService; + + @GetMapping("/user/{userId}") + public ResponseEntity> getAllGroupsByUser(@PathVariable Long userId, + Authentication authentication) { + List groups = groupService.getAllGroupsByUser(userId); + return ResponseEntity.ok(groups); + } + + @GetMapping("/{groupId}") + public ResponseEntity getGroupById(@PathVariable Long groupId, Authentication authentication) { + GroupResponse group = groupService.getGroupById(groupId); + return ResponseEntity.ok(group); + } + + @PostMapping("/user/{userId}") + public ResponseEntity addGroupToUser(@PathVariable Long userId, + @RequestBody GroupPayload groupPayload, + Authentication authentication) { + GroupResponse createdGroup = groupService.addGroupToUser(userId, groupPayload); + return ResponseEntity.ok(createdGroup); + } + + @PutMapping("/{groupId}") + public ResponseEntity updateGroup(@PathVariable Long groupId, @RequestBody GroupPayload groupPayload, + Authentication authentication) { + GroupResponse group = groupService.updateGroup(groupId, groupPayload); + return ResponseEntity.ok(group); + } + + @DeleteMapping("/{groupId}") + public ResponseEntity deleteGroup(@PathVariable Long groupId, Authentication authentication) { + groupService.deleteGroup(groupId); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/user/{userId}") + public ResponseEntity deleteAllGroupsByUser(@PathVariable Long userId, Authentication authentication) { + groupService.deleteAllGroupsByUser(userId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/cf/cfteam/controllers/codeforces/PlayerController.java b/src/main/java/com/cf/cfteam/controllers/codeforces/PlayerController.java new file mode 100644 index 0000000..e0ec4d5 --- /dev/null +++ b/src/main/java/com/cf/cfteam/controllers/codeforces/PlayerController.java @@ -0,0 +1,63 @@ +package com.cf.cfteam.controllers.codeforces; + +import com.cf.cfteam.services.codeforces.PlayerService; +import com.cf.cfteam.transfer.payloads.codeforces.PlayerPayload; +import com.cf.cfteam.transfer.responses.codeforces.PlayerResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("api/cf/players") +public class PlayerController { + + private final PlayerService playerService; + + @GetMapping("/team/{teamId}") + public ResponseEntity> getAllPlayersByTeam(@PathVariable Long teamId, + Authentication authentication) { + List players = playerService.getAllPlayersByTeam(teamId); + return ResponseEntity.ok(players); + } + + @GetMapping("/{playerId}") + public ResponseEntity getPlayerById(@PathVariable Long playerId, Authentication authentication) { + PlayerResponse player = playerService.getPlayerById(playerId); + return ResponseEntity.ok(player); + } + + @PostMapping("/team/{teamId}") + public ResponseEntity addPlayerToTeam(@PathVariable Long teamId, + @RequestBody PlayerPayload playerPayload, + Authentication authentication) { + PlayerResponse player = playerService.addPlayerToTeam(teamId, playerPayload); + return ResponseEntity.ok(player); + } + + @PutMapping("/players/{playerId}/teams/{teamId}") + public ResponseEntity updatePlayerInTeam(@PathVariable Long playerId, + @PathVariable Long teamId, + @RequestBody PlayerPayload playerPayload, + Authentication authentication) { + PlayerResponse player = playerService.updatePlayerInTeam(playerId, teamId, playerPayload); + return ResponseEntity.ok(player); + } + + @DeleteMapping("/players/{playerId}/teams/{teamId}") + public ResponseEntity deletePlayerFromTeam(@PathVariable Long playerId, + @PathVariable Long teamId, + Authentication authentication) { + playerService.deletePlayerFromTeam(playerId, teamId); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/team/{teamId}") + public ResponseEntity deleteAllPlayersFromTeam(@PathVariable Long teamId, Authentication authentication) { + playerService.deleteAllPlayersFromTeam(teamId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/cf/cfteam/controllers/codeforces/TeamController.java b/src/main/java/com/cf/cfteam/controllers/codeforces/TeamController.java new file mode 100644 index 0000000..3c4ae91 --- /dev/null +++ b/src/main/java/com/cf/cfteam/controllers/codeforces/TeamController.java @@ -0,0 +1,58 @@ +package com.cf.cfteam.controllers.codeforces; + +import com.cf.cfteam.services.codeforces.TeamService; +import com.cf.cfteam.transfer.payloads.codeforces.TeamPayload; +import com.cf.cfteam.transfer.responses.codeforces.TeamResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("api/cf/teams") +public class TeamController { + + private final TeamService teamService; + + @GetMapping("/group/{groupId}") + public ResponseEntity> getAllTeamsByGroup(@PathVariable Long groupId, + Authentication authentication) { + List teams = teamService.getAllTeamsByGroup(groupId); + return ResponseEntity.ok(teams); + } + + @GetMapping("/{teamId}") + public ResponseEntity getTeamById(@PathVariable Long teamId, Authentication authentication) { + TeamResponse team = teamService.getTeamById(teamId); + return ResponseEntity.ok(team); + } + + @PostMapping("/group/{groupId}") + public ResponseEntity addTeamToGroup(@PathVariable Long groupId, @RequestBody TeamPayload teamPayload, + Authentication authentication) { + TeamResponse createdTeam = teamService.addTeamToGroup(groupId, teamPayload); + return ResponseEntity.ok(createdTeam); + } + + @PutMapping("/{teamId}") + public ResponseEntity updateTeam(@PathVariable Long teamId, @RequestBody TeamPayload teamPayload, + Authentication authentication) { + TeamResponse team = teamService.updateTeam(teamId, teamPayload); + return ResponseEntity.ok(team); + } + + @DeleteMapping("/{teamId}") + public ResponseEntity deleteTeam(@PathVariable Long teamId, Authentication authentication) { + teamService.deleteTeam(teamId); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/group/{groupId}") + public ResponseEntity deleteAllTeamsByGroup(@PathVariable Long groupId, Authentication authentication) { + teamService.deleteAllTeamsByGroup(groupId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/cf/cfteam/exceptions/client/ClientErrorException.java b/src/main/java/com/cf/cfteam/exceptions/client/ClientErrorException.java new file mode 100644 index 0000000..7eb7c97 --- /dev/null +++ b/src/main/java/com/cf/cfteam/exceptions/client/ClientErrorException.java @@ -0,0 +1,11 @@ +package com.cf.cfteam.exceptions.client; + +public class ClientErrorException extends RuntimeException { + + private final String code; + + public ClientErrorException(String code) { + super("server.error"); + this.code = code; + } +} diff --git a/src/main/java/com/cf/cfteam/exceptions/client/ServerErrorException.java b/src/main/java/com/cf/cfteam/exceptions/client/ServerErrorException.java new file mode 100644 index 0000000..8ecc9c6 --- /dev/null +++ b/src/main/java/com/cf/cfteam/exceptions/client/ServerErrorException.java @@ -0,0 +1,14 @@ +package com.cf.cfteam.exceptions.client; + +import lombok.Getter; + +@Getter +public class ServerErrorException extends RuntimeException { + + private final String code; + + public ServerErrorException(String code) { + super("server.error"); + this.code = code; + } +} diff --git a/src/main/java/com/cf/cfteam/exceptions/codeforces/GroupNotFoundException.java b/src/main/java/com/cf/cfteam/exceptions/codeforces/GroupNotFoundException.java new file mode 100644 index 0000000..c8f6b0c --- /dev/null +++ b/src/main/java/com/cf/cfteam/exceptions/codeforces/GroupNotFoundException.java @@ -0,0 +1,14 @@ +package com.cf.cfteam.exceptions.codeforces; + +import lombok.Getter; + +@Getter +public class GroupNotFoundException extends RuntimeException { + + private final Long id; + + public GroupNotFoundException(Long id) { + super("id.not_found"); + this.id = id; + } +} diff --git a/src/main/java/com/cf/cfteam/exceptions/codeforces/PlayerAlreadyInTeamException.java b/src/main/java/com/cf/cfteam/exceptions/codeforces/PlayerAlreadyInTeamException.java new file mode 100644 index 0000000..37060c3 --- /dev/null +++ b/src/main/java/com/cf/cfteam/exceptions/codeforces/PlayerAlreadyInTeamException.java @@ -0,0 +1,13 @@ +package com.cf.cfteam.exceptions.codeforces; + +import lombok.Getter; + +@Getter +public class PlayerAlreadyInTeamException extends RuntimeException { + private final String login; + + public PlayerAlreadyInTeamException(String login) { + super("player.already_in_team"); + this.login = login; + } +} diff --git a/src/main/java/com/cf/cfteam/exceptions/codeforces/PlayerNotFoundException.java b/src/main/java/com/cf/cfteam/exceptions/codeforces/PlayerNotFoundException.java new file mode 100644 index 0000000..f19e269 --- /dev/null +++ b/src/main/java/com/cf/cfteam/exceptions/codeforces/PlayerNotFoundException.java @@ -0,0 +1,14 @@ +package com.cf.cfteam.exceptions.codeforces; + +import lombok.Getter; + +@Getter +public class PlayerNotFoundException extends RuntimeException { + + private final Long id; + + public PlayerNotFoundException(Long id) { + super("id.not_found"); + this.id = id; + } +} \ No newline at end of file diff --git a/src/main/java/com/cf/cfteam/exceptions/codeforces/PlayerNotFromTeamException.java b/src/main/java/com/cf/cfteam/exceptions/codeforces/PlayerNotFromTeamException.java new file mode 100644 index 0000000..6aadeef --- /dev/null +++ b/src/main/java/com/cf/cfteam/exceptions/codeforces/PlayerNotFromTeamException.java @@ -0,0 +1,16 @@ +package com.cf.cfteam.exceptions.codeforces; + +import lombok.Getter; + +@Getter +public class PlayerNotFromTeamException extends RuntimeException { + + private final Long teamId; + private final Long playerId; + + public PlayerNotFromTeamException(Long teamId, Long playerId) { + super("player.not_from_team"); + this.playerId = playerId; + this.teamId = teamId; + } +} \ No newline at end of file diff --git a/src/main/java/com/cf/cfteam/exceptions/codeforces/TeamNotFoundException.java b/src/main/java/com/cf/cfteam/exceptions/codeforces/TeamNotFoundException.java new file mode 100644 index 0000000..4a01084 --- /dev/null +++ b/src/main/java/com/cf/cfteam/exceptions/codeforces/TeamNotFoundException.java @@ -0,0 +1,14 @@ +package com.cf.cfteam.exceptions.codeforces; + +import lombok.Getter; + +@Getter +public class TeamNotFoundException extends RuntimeException { + + private final Long id; + + public TeamNotFoundException(Long id) { + super("id.not_found"); + this.id = id; + } +} diff --git a/src/main/java/com/cf/cfteam/exceptions/security/UserNotFoundException.java b/src/main/java/com/cf/cfteam/exceptions/security/UserNotFoundException.java index a7b6510..aad1e88 100644 --- a/src/main/java/com/cf/cfteam/exceptions/security/UserNotFoundException.java +++ b/src/main/java/com/cf/cfteam/exceptions/security/UserNotFoundException.java @@ -6,9 +6,17 @@ public class UserNotFoundException extends RuntimeException { private final String login; + private final Long id; public UserNotFoundException(String login) { super("login.not_found"); this.login = login; + this.id = null; + } + + public UserNotFoundException(Long id) { + super("id.not_found"); + this.id = id; + this.login = null; } } \ No newline at end of file diff --git a/src/main/java/com/cf/cfteam/models/entities/codeforces/CfUser.java b/src/main/java/com/cf/cfteam/models/entities/codeforces/CfUser.java deleted file mode 100644 index 248a39c..0000000 --- a/src/main/java/com/cf/cfteam/models/entities/codeforces/CfUser.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.cf.cfteam.models.entities.codeforces; - -import jakarta.persistence.*; -import lombok.*; - - -@Builder -@Setter -@Getter -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode(exclude = "group") -@Entity -@Table(name = "cf_users", schema = "codeforces") -public class CfUser { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "c_name", nullable = false) - private String name; - - @Column(name = "c_description", nullable = true) - private String description; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "c_group_id", nullable = false) - private CfUsersGroup group; -} diff --git a/src/main/java/com/cf/cfteam/models/entities/codeforces/CfUsersTeam.java b/src/main/java/com/cf/cfteam/models/entities/codeforces/CfUsersTeam.java deleted file mode 100644 index 321a9a3..0000000 --- a/src/main/java/com/cf/cfteam/models/entities/codeforces/CfUsersTeam.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.cf.cfteam.models.entities.codeforces; - -import jakarta.persistence.*; -import lombok.*; - -@Builder -@Setter -@Getter -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode(exclude = "group") -@Entity -@Table(name = "t_cf_teams", schema = "codeforces") -public class CfUsersTeam { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "c_name", nullable = false) - private String name; - - @Column(name = "c_description", nullable = true) - private String description; - - @Column(name = "c_first_user_login", nullable = true) - private String firstUser; - - @Column(name = "c_second_user_login", nullable = true) - private String secondUser; - - @Column(name = "c_third_user_login", nullable = true) - private String thirdUser; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "c_group_id", nullable = false) - private CfUsersTeamsGroup group; -} diff --git a/src/main/java/com/cf/cfteam/models/entities/codeforces/CfUsersTeamsGroup.java b/src/main/java/com/cf/cfteam/models/entities/codeforces/CfUsersTeamsGroup.java deleted file mode 100644 index 9f06d8c..0000000 --- a/src/main/java/com/cf/cfteam/models/entities/codeforces/CfUsersTeamsGroup.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.cf.cfteam.models.entities.codeforces; - -import com.cf.cfteam.models.entities.security.User; -import jakarta.persistence.*; -import lombok.*; - -import java.util.List; - -@Builder -@Setter -@Getter -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode(exclude = {"user", "cfTeams"}) -@Entity -@Table(name = "t_cf_users_teams_groups", schema = "codeforces") -public class CfUsersTeamsGroup { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "c_name", nullable = false) - private String name; - - @Column(name = "c_description", nullable = true) - private String description; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "c_user_id", nullable = false) - private User user; - - @OneToMany(mappedBy = "group", cascade = CascadeType.ALL, orphanRemoval = true) - private List cfTeams; -} diff --git a/src/main/java/com/cf/cfteam/models/entities/codeforces/CfUsersGroup.java b/src/main/java/com/cf/cfteam/models/entities/codeforces/Group.java similarity index 74% rename from src/main/java/com/cf/cfteam/models/entities/codeforces/CfUsersGroup.java rename to src/main/java/com/cf/cfteam/models/entities/codeforces/Group.java index 7ae6f56..f3d63d4 100644 --- a/src/main/java/com/cf/cfteam/models/entities/codeforces/CfUsersGroup.java +++ b/src/main/java/com/cf/cfteam/models/entities/codeforces/Group.java @@ -11,10 +11,10 @@ @Getter @NoArgsConstructor @AllArgsConstructor -@EqualsAndHashCode(exclude = {"user", "cfUsers"}) +@EqualsAndHashCode(exclude = {"user", "teams"}) @Entity -@Table(name = "t_cf_users_groups", schema = "codeforces") -public class CfUsersGroup { +@Table(name = "t_groups", schema = "codeforces") +public class Group { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -23,7 +23,7 @@ public class CfUsersGroup { @Column(name = "c_name", nullable = false) private String name; - @Column(name = "c_description", nullable = true) + @Column(name = "c_description") private String description; @ManyToOne(fetch = FetchType.LAZY) @@ -31,5 +31,5 @@ public class CfUsersGroup { private User user; @OneToMany(mappedBy = "group", cascade = CascadeType.ALL, orphanRemoval = true) - private List cfUsers; + private List teams; } diff --git a/src/main/java/com/cf/cfteam/models/entities/codeforces/Player.java b/src/main/java/com/cf/cfteam/models/entities/codeforces/Player.java new file mode 100644 index 0000000..677b177 --- /dev/null +++ b/src/main/java/com/cf/cfteam/models/entities/codeforces/Player.java @@ -0,0 +1,32 @@ +package com.cf.cfteam.models.entities.codeforces; + +import jakarta.persistence.*; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Builder +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(exclude = {"teams"}) +@Entity +@Table(name = "t_players", schema = "codeforces") +public class Player { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "c_login", nullable = false) + private String login; + + @ManyToMany(cascade = CascadeType.ALL) + @JoinTable(name = "t_team_player", schema = "codeforces", + joinColumns = @JoinColumn(name = "c_player_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "c_team_id", referencedColumnName = "id")) + @Builder.Default + private List teams = new ArrayList<>(); +} diff --git a/src/main/java/com/cf/cfteam/models/entities/codeforces/Team.java b/src/main/java/com/cf/cfteam/models/entities/codeforces/Team.java new file mode 100644 index 0000000..3452560 --- /dev/null +++ b/src/main/java/com/cf/cfteam/models/entities/codeforces/Team.java @@ -0,0 +1,39 @@ +package com.cf.cfteam.models.entities.codeforces; + +import jakarta.persistence.*; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Builder +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(exclude = {"group", "players"}) +@Entity +@Table(name = "t_teams", schema = "codeforces") +public class Team { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "c_name", nullable = false) + private String name; + + @Column(name = "c_description") + private String description; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "c_group_id", nullable = false) + private Group group; + + @ManyToMany(cascade = CascadeType.ALL) + @JoinTable(name = "t_team_player", schema = "codeforces", + joinColumns = @JoinColumn(name = "c_team_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "c_player_id", referencedColumnName = "id")) + @Builder.Default + private List players = new ArrayList<>(); +} diff --git a/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/CfUserRepository.java b/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/CfUserRepository.java deleted file mode 100644 index f389a6a..0000000 --- a/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/CfUserRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.cf.cfteam.repositories.jpa.codeforces; - -import com.cf.cfteam.models.entities.codeforces.CfUser; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface CfUserRepository extends JpaRepository { -} diff --git a/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/CfUsersGroupRepository.java b/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/CfUsersGroupRepository.java deleted file mode 100644 index a112656..0000000 --- a/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/CfUsersGroupRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.cf.cfteam.repositories.jpa.codeforces; - -import com.cf.cfteam.models.entities.codeforces.CfUsersGroup; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface CfUsersGroupRepository extends JpaRepository { -} diff --git a/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/CfUsersTeamRepository.java b/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/CfUsersTeamRepository.java deleted file mode 100644 index f48ccde..0000000 --- a/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/CfUsersTeamRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.cf.cfteam.repositories.jpa.codeforces; - -import com.cf.cfteam.models.entities.codeforces.CfUsersTeam; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface CfUsersTeamRepository extends JpaRepository { -} diff --git a/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/CfUsersTeamsGroupRepository.java b/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/CfUsersTeamsGroupRepository.java deleted file mode 100644 index bade64b..0000000 --- a/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/CfUsersTeamsGroupRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.cf.cfteam.repositories.jpa.codeforces; - -import com.cf.cfteam.models.entities.codeforces.CfUsersTeamsGroup; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface CfUsersTeamsGroupRepository extends JpaRepository { -} diff --git a/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/GroupRepository.java b/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/GroupRepository.java new file mode 100644 index 0000000..94f7951 --- /dev/null +++ b/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/GroupRepository.java @@ -0,0 +1,11 @@ +package com.cf.cfteam.repositories.jpa.codeforces; + +import com.cf.cfteam.models.entities.codeforces.Group; +import com.cf.cfteam.models.entities.security.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface GroupRepository extends JpaRepository { + List findByUser(User user); +} diff --git a/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/PlayerRepository.java b/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/PlayerRepository.java new file mode 100644 index 0000000..6798af5 --- /dev/null +++ b/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/PlayerRepository.java @@ -0,0 +1,10 @@ +package com.cf.cfteam.repositories.jpa.codeforces; + +import com.cf.cfteam.models.entities.codeforces.Player; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface PlayerRepository extends JpaRepository { + Optional findByLogin(String login); +} diff --git a/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/TeamRepository.java b/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/TeamRepository.java new file mode 100644 index 0000000..20e5108 --- /dev/null +++ b/src/main/java/com/cf/cfteam/repositories/jpa/codeforces/TeamRepository.java @@ -0,0 +1,11 @@ +package com.cf.cfteam.repositories.jpa.codeforces; + +import com.cf.cfteam.models.entities.codeforces.Group; +import com.cf.cfteam.models.entities.codeforces.Team; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TeamRepository extends JpaRepository { + List findByGroup(Group group); +} diff --git a/src/main/java/com/cf/cfteam/services/client/CodeforcesClient.java b/src/main/java/com/cf/cfteam/services/client/CodeforcesClient.java new file mode 100644 index 0000000..1ed454f --- /dev/null +++ b/src/main/java/com/cf/cfteam/services/client/CodeforcesClient.java @@ -0,0 +1,45 @@ +package com.cf.cfteam.services.client; + + +import com.cf.cfteam.exceptions.client.ClientErrorException; +import com.cf.cfteam.exceptions.client.ServerErrorException; +import lombok.RequiredArgsConstructor; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; + +@Service +@RequiredArgsConstructor +public class CodeforcesClient { + + private final RestClient restClient; + + @Value("${codeforces.api.player.url}") + private String playerUrl; + + public Double fetchRatingFromApi(String login) { + ResponseEntity playerResponse = restClient.get() + .uri(uriBuilder -> uriBuilder + .path(playerUrl) + .queryParam("handles", login) + .queryParam("checkHistoricHandles", false) + .build()) + .retrieve() + .toEntity(UserInfoResponse.class); + if (playerResponse.getStatusCode().is2xxSuccessful() && playerResponse.getBody() != null + && playerResponse.getBody().getResult().size() == 1 + && playerResponse.getBody().getResult().getFirst() != null) { + return playerResponse.getBody().getResult().getFirst().getRating() != null + ? playerResponse.getBody().getResult().getFirst().getRating() : 0.; + } + if (playerResponse.getStatusCode().is4xxClientError()) { + throw new ClientErrorException(playerResponse.getStatusCode().toString()); + } + if (playerResponse.getStatusCode().is5xxServerError()) { + throw new ServerErrorException(playerResponse.getStatusCode().toString()); + } + throw new RuntimeException("Error: " + playerResponse.getStatusCode()); + } +} diff --git a/src/main/java/com/cf/cfteam/services/client/CodeforcesPlayerResponse.java b/src/main/java/com/cf/cfteam/services/client/CodeforcesPlayerResponse.java new file mode 100644 index 0000000..68f4f67 --- /dev/null +++ b/src/main/java/com/cf/cfteam/services/client/CodeforcesPlayerResponse.java @@ -0,0 +1,16 @@ +package com.cf.cfteam.services.client; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CodeforcesPlayerResponse { + + @JsonProperty("handle") + private String handle; + + @JsonProperty("rating") + private Double rating; +} diff --git a/src/main/java/com/cf/cfteam/services/client/UserInfoResponse.java b/src/main/java/com/cf/cfteam/services/client/UserInfoResponse.java new file mode 100644 index 0000000..c761839 --- /dev/null +++ b/src/main/java/com/cf/cfteam/services/client/UserInfoResponse.java @@ -0,0 +1,18 @@ +package com.cf.cfteam.services.client; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class UserInfoResponse { + + @JsonProperty("status") + private String status; + + @JsonProperty("result") + private List result; +} diff --git a/src/main/java/com/cf/cfteam/services/codeforces/GroupService.java b/src/main/java/com/cf/cfteam/services/codeforces/GroupService.java new file mode 100644 index 0000000..dbe00c8 --- /dev/null +++ b/src/main/java/com/cf/cfteam/services/codeforces/GroupService.java @@ -0,0 +1,74 @@ +package com.cf.cfteam.services.codeforces; + +import com.cf.cfteam.exceptions.codeforces.GroupNotFoundException; +import com.cf.cfteam.exceptions.security.UserNotFoundException; +import com.cf.cfteam.transfer.responses.codeforces.GroupResponse; +import com.cf.cfteam.utils.codeforces.mappers.GroupMapper; +import com.cf.cfteam.models.entities.codeforces.Group; +import com.cf.cfteam.models.entities.security.User; +import com.cf.cfteam.repositories.jpa.codeforces.GroupRepository; +import com.cf.cfteam.repositories.jpa.security.UserRepository; +import com.cf.cfteam.transfer.payloads.codeforces.GroupPayload; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class GroupService { + + private final GroupRepository groupRepository; + private final UserRepository userRepository; + private final GroupMapper groupMapper; + + public List getAllGroupsByUser(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + + List groups = groupRepository.findByUser(user); + + return groups.stream() + .map(groupMapper::fromEntityToResponse) + .toList(); + } + + public GroupResponse getGroupById(Long groupId) { + Group group = groupRepository.findById(groupId) + .orElseThrow(() -> new GroupNotFoundException(groupId)); + + return groupMapper.fromEntityToResponse(group); + } + + public GroupResponse addGroupToUser(Long userId, GroupPayload groupPayload) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + + Group group = groupMapper.fromPayloadToEntity(groupPayload, user); + group = groupRepository.save(group); + + return groupMapper.fromEntityToResponse(group); + } + + public GroupResponse updateGroup(Long groupId, GroupPayload groupPayload) { + Group group = groupRepository.findById(groupId) + .orElseThrow(() -> new GroupNotFoundException(groupId)); + + Group updatedGroup = groupMapper.updateEntityFromPayload(group, groupPayload); + updatedGroup = groupRepository.save(updatedGroup); + + return groupMapper.fromEntityToResponse(updatedGroup); + } + + + public void deleteGroup(Long groupId) { + groupRepository.deleteById(groupId); + } + + public void deleteAllGroupsByUser(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + List groups = groupRepository.findByUser(user); + groupRepository.deleteAll(groups); + } +} diff --git a/src/main/java/com/cf/cfteam/services/codeforces/PlayerService.java b/src/main/java/com/cf/cfteam/services/codeforces/PlayerService.java new file mode 100644 index 0000000..50bed0d --- /dev/null +++ b/src/main/java/com/cf/cfteam/services/codeforces/PlayerService.java @@ -0,0 +1,109 @@ +package com.cf.cfteam.services.codeforces; + +import com.cf.cfteam.exceptions.codeforces.PlayerAlreadyInTeamException; +import com.cf.cfteam.exceptions.codeforces.PlayerNotFoundException; +import com.cf.cfteam.exceptions.codeforces.PlayerNotFromTeamException; +import com.cf.cfteam.exceptions.codeforces.TeamNotFoundException; +import com.cf.cfteam.models.entities.codeforces.Player; +import com.cf.cfteam.repositories.jpa.codeforces.PlayerRepository; +import com.cf.cfteam.repositories.jpa.codeforces.TeamRepository; +import com.cf.cfteam.transfer.payloads.codeforces.PlayerPayload; +import com.cf.cfteam.transfer.responses.codeforces.PlayerResponse; +import com.cf.cfteam.utils.codeforces.mappers.PlayerMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class PlayerService { + + private final PlayerRepository playerRepository; + private final TeamRepository teamRepository; + private final PlayerMapper playerMapper; + private final TeamPlayerLinker teamPlayerLinker; + + public List getAllPlayersByTeam(Long teamId) { + var team = teamRepository.findById(teamId) + .orElseThrow(() -> new TeamNotFoundException(teamId)); + + return team.getPlayers().stream() + .map(playerMapper::fromEntityToResponse) + .toList(); + } + + public PlayerResponse getPlayerById(Long playerId) { + var player = playerRepository.findById(playerId) + .orElseThrow(() -> new PlayerNotFoundException(playerId)); + + return playerMapper.fromEntityToResponse(player); + } + + public PlayerResponse addPlayerToTeam(Long teamId, PlayerPayload payload) { + var team = teamRepository.findById(teamId) + .orElseThrow(() -> new TeamNotFoundException(teamId)); + + var player = createPlayer(payload); + + if (team.getPlayers().contains(player)) { + throw new PlayerAlreadyInTeamException(player.getLogin()); + } + + teamPlayerLinker.linkTeamAndPlayer(team, player); + + return playerMapper.fromEntityToResponse(player); + } + + + public PlayerResponse updatePlayerInTeam(Long playerId, Long teamId, PlayerPayload payload) { + var player = playerRepository.findById(playerId) + .orElseThrow(() -> new PlayerNotFoundException(playerId)); + + var team = teamRepository.findById(teamId) + .orElseThrow(() -> new TeamNotFoundException(teamId)); + + if (!team.getPlayers().contains(player)) { + throw new PlayerNotFromTeamException(teamId, playerId); + } + + teamPlayerLinker.unlinkTeamAndPlayer(team, player); + player = createPlayer(payload); + teamPlayerLinker.linkTeamAndPlayer(team, player); + + return playerMapper.fromEntityToResponse(player); + } + + private Player createPlayer(PlayerPayload payload) { + var exist = playerRepository.findByLogin(payload.login()); + if (exist.isPresent()) return exist.get(); + + Player player = playerMapper.fromPayloadToEntity(payload); + return playerRepository.save(player); + } + + + public void deletePlayerFromTeam(Long playerId, Long teamId) { + var player = playerRepository.findById(playerId) + .orElseThrow(() -> new PlayerNotFoundException(playerId)); + + var team = teamRepository.findById(teamId) + .orElseThrow(() -> new TeamNotFoundException(teamId)); + + teamPlayerLinker.unlinkTeamAndPlayer(team, player); + } + + + public void deleteAllPlayersFromTeam(Long teamId) { + var team = teamRepository.findById(teamId) + .orElseThrow(() -> new TeamNotFoundException(teamId)); + + var players = List.copyOf(team.getPlayers()); + + players.forEach(player -> player.getTeams().remove(team)); + team.getPlayers().clear(); + + teamRepository.save(team); + players.forEach(playerRepository::save); + } +} diff --git a/src/main/java/com/cf/cfteam/services/codeforces/TeamPlayerLinker.java b/src/main/java/com/cf/cfteam/services/codeforces/TeamPlayerLinker.java new file mode 100644 index 0000000..8086027 --- /dev/null +++ b/src/main/java/com/cf/cfteam/services/codeforces/TeamPlayerLinker.java @@ -0,0 +1,33 @@ +package com.cf.cfteam.services.codeforces; + +import com.cf.cfteam.models.entities.codeforces.Player; +import com.cf.cfteam.models.entities.codeforces.Team; +import com.cf.cfteam.repositories.jpa.codeforces.PlayerRepository; +import com.cf.cfteam.repositories.jpa.codeforces.TeamRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class TeamPlayerLinker { + + private final TeamRepository teamRepository; + private final PlayerRepository playerRepository; + + public void unlinkTeamAndPlayer(Team team, Player player) { + team.getPlayers().remove(player); + player.getTeams().remove(team); + saveTeamAndPlayer(team, player); + } + + public void linkTeamAndPlayer(Team team, Player player) { + team.getPlayers().add(player); + player.getTeams().add(team); + saveTeamAndPlayer(team, player); + } + + public void saveTeamAndPlayer(Team team, Player player) { + teamRepository.save(team); + playerRepository.save(player); + } +} diff --git a/src/main/java/com/cf/cfteam/services/codeforces/TeamService.java b/src/main/java/com/cf/cfteam/services/codeforces/TeamService.java new file mode 100644 index 0000000..b184b06 --- /dev/null +++ b/src/main/java/com/cf/cfteam/services/codeforces/TeamService.java @@ -0,0 +1,73 @@ +package com.cf.cfteam.services.codeforces; + +import com.cf.cfteam.exceptions.codeforces.GroupNotFoundException; +import com.cf.cfteam.exceptions.codeforces.TeamNotFoundException; +import com.cf.cfteam.models.entities.codeforces.Group; +import com.cf.cfteam.models.entities.codeforces.Team; +import com.cf.cfteam.repositories.jpa.codeforces.GroupRepository; +import com.cf.cfteam.repositories.jpa.codeforces.TeamRepository; +import com.cf.cfteam.transfer.payloads.codeforces.TeamPayload; +import com.cf.cfteam.transfer.responses.codeforces.TeamResponse; +import com.cf.cfteam.utils.codeforces.mappers.TeamMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class TeamService { + + private final TeamRepository teamRepository; + private final GroupRepository groupRepository; + private final TeamMapper teamMapper; + + public List getAllTeamsByGroup(Long groupId) { + Group group = groupRepository.findById(groupId) + .orElseThrow(() -> new GroupNotFoundException(groupId)); + + List teams = teamRepository.findByGroup(group); + + return teams.stream() + .map(teamMapper::fromEntityToResponse) + .toList(); + } + + public TeamResponse getTeamById(Long teamId) { + Team team = teamRepository.findById(teamId) + .orElseThrow(() -> new TeamNotFoundException(teamId)); + + return teamMapper.fromEntityToResponse(team); + } + + public TeamResponse addTeamToGroup(Long groupId, TeamPayload teamPayload) { + Group group = groupRepository.findById(groupId) + .orElseThrow(() -> new GroupNotFoundException(groupId)); + + Team team = teamMapper.fromPayloadToEntity(teamPayload, group); + team = teamRepository.save(team); + + return teamMapper.fromEntityToResponse(team); + } + + public TeamResponse updateTeam(Long teamId, TeamPayload teamPayload) { + Team team = teamRepository.findById(teamId) + .orElseThrow(() -> new TeamNotFoundException(teamId)); + + Team updatedTeam = teamMapper.updateEntityFromPayload(team, teamPayload); + updatedTeam = teamRepository.save(updatedTeam); + + return teamMapper.fromEntityToResponse(updatedTeam); + } + + public void deleteTeam(Long teamId) { + teamRepository.deleteById(teamId); + } + + public void deleteAllTeamsByGroup(Long groupId) { + Group group = groupRepository.findById(groupId) + .orElseThrow(() -> new GroupNotFoundException(groupId)); + List teams = teamRepository.findByGroup(group); + teamRepository.deleteAll(teams); + } +} diff --git a/src/main/java/com/cf/cfteam/transfer/payloads/codeforces/GroupPayload.java b/src/main/java/com/cf/cfteam/transfer/payloads/codeforces/GroupPayload.java new file mode 100644 index 0000000..808eac8 --- /dev/null +++ b/src/main/java/com/cf/cfteam/transfer/payloads/codeforces/GroupPayload.java @@ -0,0 +1,10 @@ +package com.cf.cfteam.transfer.payloads.codeforces; + +import lombok.Builder; + +@Builder +public record GroupPayload( + String name, + String description +) { +} diff --git a/src/main/java/com/cf/cfteam/transfer/payloads/codeforces/PlayerPayload.java b/src/main/java/com/cf/cfteam/transfer/payloads/codeforces/PlayerPayload.java new file mode 100644 index 0000000..ea2dff7 --- /dev/null +++ b/src/main/java/com/cf/cfteam/transfer/payloads/codeforces/PlayerPayload.java @@ -0,0 +1,9 @@ +package com.cf.cfteam.transfer.payloads.codeforces; + +import lombok.Builder; + +@Builder +public record PlayerPayload( + String login +) { +} diff --git a/src/main/java/com/cf/cfteam/transfer/payloads/codeforces/TeamPayload.java b/src/main/java/com/cf/cfteam/transfer/payloads/codeforces/TeamPayload.java new file mode 100644 index 0000000..3b6d0ed --- /dev/null +++ b/src/main/java/com/cf/cfteam/transfer/payloads/codeforces/TeamPayload.java @@ -0,0 +1,10 @@ +package com.cf.cfteam.transfer.payloads.codeforces; + +import lombok.Builder; + +@Builder +public record TeamPayload( + String name, + String description +) { +} diff --git a/src/main/java/com/cf/cfteam/transfer/responses/codeforces/GroupResponse.java b/src/main/java/com/cf/cfteam/transfer/responses/codeforces/GroupResponse.java new file mode 100644 index 0000000..6c76729 --- /dev/null +++ b/src/main/java/com/cf/cfteam/transfer/responses/codeforces/GroupResponse.java @@ -0,0 +1,14 @@ +package com.cf.cfteam.transfer.responses.codeforces; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record GroupResponse( + Long id, + String name, + String description, + List teams +) { +} diff --git a/src/main/java/com/cf/cfteam/transfer/responses/codeforces/PlayerResponse.java b/src/main/java/com/cf/cfteam/transfer/responses/codeforces/PlayerResponse.java new file mode 100644 index 0000000..eae701a --- /dev/null +++ b/src/main/java/com/cf/cfteam/transfer/responses/codeforces/PlayerResponse.java @@ -0,0 +1,11 @@ +package com.cf.cfteam.transfer.responses.codeforces; + +import lombok.Builder; + +@Builder +public record PlayerResponse( + Long id, + String login, + Double rating +) { +} diff --git a/src/main/java/com/cf/cfteam/transfer/responses/codeforces/TeamResponse.java b/src/main/java/com/cf/cfteam/transfer/responses/codeforces/TeamResponse.java new file mode 100644 index 0000000..56b2f5b --- /dev/null +++ b/src/main/java/com/cf/cfteam/transfer/responses/codeforces/TeamResponse.java @@ -0,0 +1,15 @@ +package com.cf.cfteam.transfer.responses.codeforces; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record TeamResponse( + Long id, + String name, + String description, + List players, + Double teamRating +) { +} diff --git a/src/main/java/com/cf/cfteam/transfer/responses/security/UserResponse.java b/src/main/java/com/cf/cfteam/transfer/responses/security/UserResponse.java new file mode 100644 index 0000000..98f122c --- /dev/null +++ b/src/main/java/com/cf/cfteam/transfer/responses/security/UserResponse.java @@ -0,0 +1,4 @@ +package com.cf.cfteam.transfer.responses.security; + +public class UserResponse { +} diff --git a/src/main/java/com/cf/cfteam/utils/ErrorResponseBuilder.java b/src/main/java/com/cf/cfteam/utils/ErrorResponseBuilder.java new file mode 100644 index 0000000..920ecf0 --- /dev/null +++ b/src/main/java/com/cf/cfteam/utils/ErrorResponseBuilder.java @@ -0,0 +1,30 @@ +package com.cf.cfteam.utils; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +public final class ErrorResponseBuilder { + + public static ResponseEntity buildErrorResponse(String message, HttpStatus status, + Map details) { + Map errorResponse = new HashMap<>(); + errorResponse.put("timestamp", LocalDateTime.now()); + errorResponse.put("status", status.value()); + errorResponse.put("error", status.getReasonPhrase()); + errorResponse.put("message", message); + + if (details != null) { + errorResponse.put("details", details); + } + + return new ResponseEntity<>(errorResponse, status); + } + + private ErrorResponseBuilder() { + throw new UnsupportedOperationException("Utility class"); + } +} \ No newline at end of file diff --git a/src/main/java/com/cf/cfteam/utils/codeforces/RatingCalculator.java b/src/main/java/com/cf/cfteam/utils/codeforces/RatingCalculator.java new file mode 100644 index 0000000..2f27662 --- /dev/null +++ b/src/main/java/com/cf/cfteam/utils/codeforces/RatingCalculator.java @@ -0,0 +1,40 @@ +package com.cf.cfteam.utils.codeforces; + +import com.cf.cfteam.transfer.responses.codeforces.PlayerResponse; + +import java.util.List; + +public final class RatingCalculator { + + private static double getWinProbability(double ra, double rb) { + return 1.0 / (1.0 + Math.pow(10.0, (rb - ra) / 400.0)); + } + + public static double aggregateRatings(List players) { + double left = 1.0; + double right = 1E4; + + for (int tt = 0; tt < 100; tt++) { + double r = (left + right) / 2.0; + + double rWinsProbability = 1.0; + for (PlayerResponse player : players) { + rWinsProbability *= getWinProbability(r, player.rating()); + } + + double rating = Math.log10(1 / rWinsProbability - 1) * 400 + r; + + if (rating > r) { + left = r; + } else { + right = r; + } + } + + return (left + right) / 2.0; + } + + private RatingCalculator() { + throw new UnsupportedOperationException("Utility class"); + } +} diff --git a/src/main/java/com/cf/cfteam/utils/codeforces/mappers/GroupMapper.java b/src/main/java/com/cf/cfteam/utils/codeforces/mappers/GroupMapper.java new file mode 100644 index 0000000..fa523a9 --- /dev/null +++ b/src/main/java/com/cf/cfteam/utils/codeforces/mappers/GroupMapper.java @@ -0,0 +1,50 @@ +package com.cf.cfteam.utils.codeforces.mappers; + +import com.cf.cfteam.models.entities.codeforces.Group; +import com.cf.cfteam.models.entities.security.User; +import com.cf.cfteam.transfer.payloads.codeforces.GroupPayload; +import com.cf.cfteam.transfer.responses.codeforces.GroupResponse; +import com.cf.cfteam.transfer.responses.codeforces.TeamResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +@RequiredArgsConstructor +public final class GroupMapper { + + private final TeamMapper teamMapper; + + public Group fromPayloadToEntity(GroupPayload payload, User user) { + return Group.builder() + .name(payload.name()) + .description(payload.description()) + .user(user) + .build(); + } + + public Group updateEntityFromPayload(Group group, GroupPayload payload) { + group.setName(payload.name()); + group.setDescription(payload.description()); + return group; + } + + public GroupResponse fromEntityToResponse(Group group) { + List convertedTeams = new ArrayList<>(); + + if (group.getTeams() != null) { + convertedTeams = group.getTeams().stream() + .map(teamMapper::fromEntityToResponse) + .toList(); + } + + return GroupResponse.builder() + .id(group.getId()) + .name(group.getName()) + .description(group.getDescription()) + .teams(convertedTeams) + .build(); + } +} diff --git a/src/main/java/com/cf/cfteam/utils/codeforces/mappers/PlayerMapper.java b/src/main/java/com/cf/cfteam/utils/codeforces/mappers/PlayerMapper.java new file mode 100644 index 0000000..9f61398 --- /dev/null +++ b/src/main/java/com/cf/cfteam/utils/codeforces/mappers/PlayerMapper.java @@ -0,0 +1,29 @@ +package com.cf.cfteam.utils.codeforces.mappers; + +import com.cf.cfteam.models.entities.codeforces.Player; +import com.cf.cfteam.transfer.payloads.codeforces.PlayerPayload; +import com.cf.cfteam.transfer.responses.codeforces.PlayerResponse; +import com.github.benmanes.caffeine.cache.LoadingCache; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class PlayerMapper { + + private final LoadingCache ratingCache; + + public PlayerResponse fromEntityToResponse(Player player) { + return PlayerResponse.builder() + .id(player.getId()) + .login(player.getLogin()) + .rating(ratingCache.get(player.getLogin())) + .build(); + } + + public Player fromPayloadToEntity(PlayerPayload payload) { + return Player.builder() + .login(payload.login()) + .build(); + } +} diff --git a/src/main/java/com/cf/cfteam/utils/codeforces/mappers/TeamMapper.java b/src/main/java/com/cf/cfteam/utils/codeforces/mappers/TeamMapper.java new file mode 100644 index 0000000..875ad64 --- /dev/null +++ b/src/main/java/com/cf/cfteam/utils/codeforces/mappers/TeamMapper.java @@ -0,0 +1,55 @@ +package com.cf.cfteam.utils.codeforces.mappers; + +import com.cf.cfteam.models.entities.codeforces.Group; +import com.cf.cfteam.models.entities.codeforces.Team; +import com.cf.cfteam.transfer.payloads.codeforces.TeamPayload; +import com.cf.cfteam.transfer.responses.codeforces.PlayerResponse; +import com.cf.cfteam.transfer.responses.codeforces.TeamResponse; +import com.cf.cfteam.utils.codeforces.RatingCalculator; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class TeamMapper { + + private final PlayerMapper playerMapper; + + public TeamResponse fromEntityToResponse(Team team) { + List convertedPlayers = new ArrayList<>(); + Double teamRating = 0.; + + if (team.getPlayers() != null && !team.getPlayers().isEmpty()) { + convertedPlayers = team.getPlayers().stream() + .map(playerMapper::fromEntityToResponse) + .toList(); + + teamRating = RatingCalculator.aggregateRatings(convertedPlayers); + } + + return TeamResponse.builder() + .id(team.getId()) + .name(team.getName()) + .description(team.getDescription()) + .players(convertedPlayers) + .teamRating(teamRating) + .build(); + } + + public Team fromPayloadToEntity(TeamPayload payload, Group group) { + return Team.builder() + .group(group) + .name(payload.name()) + .description(payload.description()) + .build(); + } + + public Team updateEntityFromPayload(Team team, TeamPayload payload) { + team.setName(payload.name()); + team.setDescription(payload.description()); + return team; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3f1b622..a581d4c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -7,7 +7,7 @@ spring: password: ${POSTGRES_PASSWORD:123} driver-class-name: org.postgresql.Driver config: - import: jwt.yml + import: jwt.yml, codeforces.yml profiles: active: prod diff --git a/src/main/resources/codeforces.yml b/src/main/resources/codeforces.yml new file mode 100644 index 0000000..436380b --- /dev/null +++ b/src/main/resources/codeforces.yml @@ -0,0 +1,6 @@ +codeforces: + api: + base: + url: https://codeforces.com/api + player: + url: /user.info \ No newline at end of file diff --git a/src/main/resources/db/changelog/16-12-24-2.sql b/src/main/resources/db/changelog/16-12-24-2.sql new file mode 100644 index 0000000..89043f2 --- /dev/null +++ b/src/main/resources/db/changelog/16-12-24-2.sql @@ -0,0 +1,11 @@ +--liquibase formatted sql + +--changeset alexandergarifullin:33 +ALTER TABLE codeforces.t_team_player_match DROP COLUMN c_description; + +--changeset alexandergarifullin:34 +ALTER TABLE codeforces.t_team_player_match RENAME TO t_team_player; + +--changeset alexandergarifullin:35 +CREATE INDEX idx_players_c_login ON codeforces.t_players(c_login); + diff --git a/src/main/resources/db/changelog/16-12-24-set-new-cf-schema.sql b/src/main/resources/db/changelog/16-12-24-set-new-cf-schema.sql new file mode 100644 index 0000000..62b04a7 --- /dev/null +++ b/src/main/resources/db/changelog/16-12-24-set-new-cf-schema.sql @@ -0,0 +1,45 @@ +--liquibase formatted sql + +--changeset alexandergarifullin:25 +ALTER TABLE codeforces.t_cf_users_teams_groups RENAME TO t_groups; + +--changeset alexandergarifullin:26 +DROP TABLE IF EXISTS codeforces.t_cf_teams CASCADE; + +--changeset alexandergarifullin:27 +DROP TABLE IF EXISTS codeforces.t_cf_users_groups CASCADE; + +--changeset alexandergarifullin:28 +DROP TABLE IF EXISTS codeforces.cf_users CASCADE; + +--changeset alexandergarifullin:29 +CREATE TABLE codeforces.t_players +( + id BIGSERIAL PRIMARY KEY, + c_login TEXT NOT NULL UNIQUE +); + +--changeset alexandergarifullin:31 +CREATE TABLE codeforces.t_teams +( + id BIGSERIAL PRIMARY KEY, + c_name TEXT NOT NULL, + c_description TEXT NULL, + c_group_id BIGINT NOT NULL, + CONSTRAINT fk_team_group FOREIGN KEY (c_group_id ) REFERENCES codeforces.t_groups (id) +); + +--changeset alexandergarifullin:32 +CREATE TABLE codeforces.t_team_player_match +( + id BIGSERIAL PRIMARY KEY, + c_description TEXT NOT NULL, + c_team_id BIGINT NOT NULL, + c_player_id BIGINT NOT NULL, + CONSTRAINT fk_match_team FOREIGN KEY (c_team_id) REFERENCES codeforces.t_teams (id), + CONSTRAINT fk_match_player FOREIGN KEY (c_player_id) REFERENCES codeforces.t_players (id) +); + + + + diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index e37ca76..b02ee85 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -10,4 +10,9 @@ databaseChangeLog: - include: file: db/changelog/set-ctime-default-values.sql - include: - file: db/changelog/remove-ctime.sql \ No newline at end of file + file: db/changelog/remove-ctime.sql + - include: + file: db/changelog/16-12-24-set-new-cf-schema.sql + - include: + file: db/changelog/16-12-24-2.sql + diff --git a/src/main/resources/db/changelog/db.changelog-test.yaml b/src/main/resources/db/changelog/db.changelog-test.yaml index d404583..e49b392 100644 --- a/src/main/resources/db/changelog/db.changelog-test.yaml +++ b/src/main/resources/db/changelog/db.changelog-test.yaml @@ -12,4 +12,8 @@ databaseChangeLog: - include: file: db/changelog/remove-ctime.sql - include: - file: db/changelog/init-security-changelog.sql \ No newline at end of file + file: db/changelog/init-security-changelog.sql + - include: + file: db/changelog/16-12-24-set-new-cf-schema.sql + - include: + file: db/changelog/16-12-24-2.sql \ No newline at end of file diff --git a/src/test/java/com/cf/cfteam/controllers/codeforces/integration/GroupControllerIntegrationTest.java b/src/test/java/com/cf/cfteam/controllers/codeforces/integration/GroupControllerIntegrationTest.java new file mode 100644 index 0000000..9cd44a1 --- /dev/null +++ b/src/test/java/com/cf/cfteam/controllers/codeforces/integration/GroupControllerIntegrationTest.java @@ -0,0 +1,341 @@ +package com.cf.cfteam.controllers.codeforces.integration; + +import com.cf.cfteam.transfer.responses.codeforces.GroupResponse; +import com.cf.cfteam.utils.codeforces.mappers.GroupMapper; +import com.fasterxml.jackson.core.type.TypeReference; +import com.cf.cfteam.BaseIntegrationTest; +import com.cf.cfteam.models.entities.codeforces.Group; +import com.cf.cfteam.models.entities.security.Role; +import com.cf.cfteam.models.entities.security.User; +import com.cf.cfteam.repositories.jpa.codeforces.GroupRepository; +import com.cf.cfteam.repositories.jpa.security.UserRepository; +import com.cf.cfteam.transfer.payloads.codeforces.GroupPayload; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@ActiveProfiles("test") +public class GroupControllerIntegrationTest extends BaseIntegrationTest { + + private static final String URI = "/api/cf/groups"; + + private static final Long BAD_USER_ID = 9999L; + private static final Long BAD_GROUP_ID = 9999L; + + @Autowired + private GroupRepository groupRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private GroupMapper groupMapper; + + @Test + @SneakyThrows + public void getAllGroupsByUser_notEmpty_success() { + User user = createUser(); + user = userRepository.save(user); + + Group group = createGroup(user); + group = groupRepository.save(group); + + + var mvcResponse = mockMvc.perform(get(URI + "/user/" + user.getId()) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + var groups = objectMapper.readValue( + mvcResponse.getContentAsString(), + new TypeReference>() { + } + ); + + assertThat(groups).hasSize(1) + .contains(groupMapper.fromEntityToResponse(group)); + + deleteGroupFromDb(group); + deleteUserFromDb(user); + } + + @Test + @SneakyThrows + void getAllGroupsByUser_shouldThrowUserNotFound_whenUserNotFound() { + mockMvc.perform(get(URI + "/user/" + BAD_USER_ID) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isNotFound(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.id").value(BAD_USER_ID), + jsonPath("$.message").value("id.not_found"), + jsonPath("$.error").value("Not Found"), + jsonPath("$.status").value(404) + ); + } + + @Test + @SneakyThrows + public void getGroupById_notEmpty() { + User user = createUser(); + user = userRepository.save(user); + + Group group = createGroup(user); + group = groupRepository.save(group); + + var mvcResponse = mockMvc.perform(get(URI + "/" + group.getId()) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + var responseGroupe = objectMapper.readValue(mvcResponse.getContentAsString(), GroupResponse.class); + + assertThat(responseGroupe).isEqualTo(groupMapper.fromEntityToResponse(group)); + + deleteGroupFromDb(group); + deleteUserFromDb(user); + } + + @Test + @SneakyThrows + void getGroupById_shouldThrowGroupNotFound_whenGroupNotFound() { + mockMvc.perform(get(URI + "/" + BAD_USER_ID) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isNotFound(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.id").value(BAD_USER_ID), + jsonPath("$.message").value("id.not_found"), + jsonPath("$.error").value("Not Found"), + jsonPath("$.status").value(404) + ); + } + + @Test + @SneakyThrows + public void addGroupToUser_success() { + User user = createUser(); + user = userRepository.save(user); + + var payload = createGroupPayload(); + + var mvcResponse = mockMvc.perform(post(URI + "/user/" + user.getId()) + .header("Authorization", userBearerToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + var responseGroupe = objectMapper.readValue(mvcResponse.getContentAsString(), GroupResponse.class); + var groupFromDb = groupRepository.findById(responseGroupe.id()); + + assertAll( + () -> assertThat(groupFromDb).isPresent(), + () -> assertThat(responseGroupe.description()).isEqualTo(payload.description()), + () -> assertThat(responseGroupe.name()).isEqualTo(payload.name()) + ); + + deleteGroupFromDb(groupFromDb.get()); + deleteUserFromDb(user); + } + + @Test + @SneakyThrows + void addGroupToUser_shouldThrowUserNotFound_whenUserNotFound() { + var payload = createGroupPayload(); + + mockMvc.perform(post(URI + "/user/" + BAD_USER_ID) + .header("Authorization", userBearerToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpectAll( + status().isNotFound(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.id").value(BAD_USER_ID), + jsonPath("$.message").value("id.not_found"), + jsonPath("$.error").value("Not Found"), + jsonPath("$.status").value(404) + ); + } + + @Test + @SneakyThrows + public void updateGroup_success() { + User user = createUser(); + user = userRepository.save(user); + + var payload = createGroupPayload("new name", "new description"); + + Group group = createGroup(user); + group = groupRepository.save(group); + + + var mvcResponse = mockMvc.perform(put(URI + "/" + group.getId()) + .header("Authorization", userBearerToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + + var responseGroupe = objectMapper.readValue(mvcResponse.getContentAsString(), GroupResponse.class); + var groupFromDb = groupRepository.findById(responseGroupe.id()); + + assertAll( + () -> assertThat(groupFromDb).isPresent(), + () -> assertThat(groupFromDb.get().getDescription()).isEqualTo(payload.description()), + () -> assertThat(groupFromDb.get().getName()).isEqualTo(payload.name()) + ); + + deleteGroupFromDb(groupFromDb.get()); + deleteUserFromDb(user); + } + + @Test + @SneakyThrows + void updateGroup_shouldThrowGroupNotFound_whenGroupNotFound() { + var payload = createGroupPayload(); + + mockMvc.perform(put(URI + "/" + BAD_GROUP_ID) + .header("Authorization", userBearerToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpectAll( + status().isNotFound(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.id").value(BAD_USER_ID), + jsonPath("$.message").value("id.not_found"), + jsonPath("$.error").value("Not Found"), + jsonPath("$.status").value(404) + ); + } + + @Test + @SneakyThrows + public void deleteGroup_success() { + User user = createUser(); + user = userRepository.save(user); + + Group group = createGroup(user); + group = groupRepository.save(group); + + mockMvc.perform(delete(URI + "/" + group.getId()) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isNoContent(), + content().string("") + ); + + var groupFromDb = groupRepository.findById(group.getId()); + + assertThat(groupFromDb).isNotPresent(); + + deleteUserFromDb(user); + } + + @Test + @SneakyThrows + public void deleteAllGroupsByUser_success() { + User user = createUser(); + user = userRepository.save(user); + + Group group1 = createGroup(user); + group1 = groupRepository.save(group1); + + Group group2 = createGroup(user); + group2 = groupRepository.save(group2); + + mockMvc.perform(delete(URI + "/user/" + user.getId()) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isNoContent(), + content().string("") + ); + + var groupFromDb1 = groupRepository.findById(group1.getId()); + var groupFromDb2 = groupRepository.findById(group2.getId()); + + assertAll( + () -> assertThat(groupFromDb1).isNotPresent(), + () -> assertThat(groupFromDb2).isNotPresent() + ); + + var mvcResponse = mockMvc.perform(get(URI + "/user/" + user.getId()) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + var groups = objectMapper.readValue( + mvcResponse.getContentAsString(), + new TypeReference>() { + } + ); + + assertThat(groups).isEmpty(); + + deleteUserFromDb(user); + } + + private User createUser() { + return User.builder() + .name("User name") + .login("User login") + .hashedPassword("Password") + .role(Role.USER) + .build(); + } + + private Group createGroup(User user) { + return Group.builder() + .name("Test Group") + .description("Test description") + .user(user) + .build(); + } + + private GroupPayload createGroupPayload() { + return GroupPayload.builder() + .name("Test group") + .description("Test description") + .build(); + } + + private GroupPayload createGroupPayload(String name, String description) { + return GroupPayload.builder() + .name(name) + .description(description) + .build(); + } + + private void deleteUserFromDb(User user) { + userRepository.delete(user); + } + + private void deleteGroupFromDb(Group group) { + groupRepository.delete(group); + } +} diff --git a/src/test/java/com/cf/cfteam/controllers/codeforces/integration/PlayerControllerIntegrationTest.java b/src/test/java/com/cf/cfteam/controllers/codeforces/integration/PlayerControllerIntegrationTest.java new file mode 100644 index 0000000..b7d96e9 --- /dev/null +++ b/src/test/java/com/cf/cfteam/controllers/codeforces/integration/PlayerControllerIntegrationTest.java @@ -0,0 +1,630 @@ +package com.cf.cfteam.controllers.codeforces.integration; + +import com.cf.cfteam.BaseIntegrationTest; +import com.cf.cfteam.models.entities.codeforces.Group; +import com.cf.cfteam.models.entities.codeforces.Player; +import com.cf.cfteam.models.entities.codeforces.Team; +import com.cf.cfteam.models.entities.security.Role; +import com.cf.cfteam.models.entities.security.User; +import com.cf.cfteam.repositories.jpa.codeforces.GroupRepository; +import com.cf.cfteam.repositories.jpa.codeforces.PlayerRepository; +import com.cf.cfteam.repositories.jpa.codeforces.TeamRepository; +import com.cf.cfteam.repositories.jpa.security.UserRepository; +import com.cf.cfteam.transfer.payloads.codeforces.PlayerPayload; +import com.cf.cfteam.transfer.responses.codeforces.PlayerResponse; +import com.cf.cfteam.utils.codeforces.mappers.PlayerMapper; +import com.fasterxml.jackson.core.type.TypeReference; +import com.github.benmanes.caffeine.cache.LoadingCache; +import jakarta.transaction.Transactional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; + + +@ActiveProfiles("test") +class PlayerControllerIntegrationTest extends BaseIntegrationTest { + + private static final String URI = "/api/cf/players"; + + private static final Long BAD_TEAM_ID = 9999L; + private static final Long BAD_PLAYER_ID = 9999L; + + + @Autowired + private PlayerRepository playerRepository; + + @Autowired + private TeamRepository teamRepository; + + @Autowired + private GroupRepository groupRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private PlayerMapper playerMapper; + + @MockBean + private LoadingCache ratingCache; + + @Test + @SneakyThrows + public void getAllPlayersByTeam_notEmpty_success() { + User user = createUser("getAllPlayersByTeam_notEmpty_success"); + user = userRepository.save(user); + + Group group = createGroup(user); + group = groupRepository.save(group); + + Team team = createTeam(group); + team = teamRepository.save(team); + + Player player = createPlayer(); + player = playerRepository.save(player); + + team.setPlayers(List.of(player)); + team = teamRepository.save(team); + + when(ratingCache.get(player.getLogin())).thenReturn(0.); + + var mvcResponse = mockMvc.perform(get(URI + "/team/" + team.getId()) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + var players = objectMapper.readValue( + mvcResponse.getContentAsString(), + new TypeReference>() { + } + ); + + assertThat(players).hasSize(1) + .contains(playerMapper.fromEntityToResponse(player)); + + deletePlayer(player); + deleteTeamFromDb(team); + deleteGroupFromDb(group); + deleteUserFromDb(user); + } + + @Test + @SneakyThrows + void getAllPlayersByTeam_shouldThrowTeamNotFoundException_whenTeamNotFoundException() { + mockMvc.perform(get(URI + "/team/" + BAD_TEAM_ID) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isNotFound(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.id").value(BAD_TEAM_ID), + jsonPath("$.message").value("id.not_found"), + jsonPath("$.error").value("Not Found"), + jsonPath("$.status").value(404) + ); + } + + @Test + @SneakyThrows + public void getPlayerById_notEmpty() { + Player player = createPlayer(); + player = playerRepository.save(player); + when(ratingCache.get(player.getLogin())).thenReturn(0.); + + var mvcResponse = mockMvc.perform(get(URI + "/" + player.getId()) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + var response = objectMapper.readValue(mvcResponse.getContentAsString(), PlayerResponse.class); + + assertThat(response).isEqualTo(playerMapper.fromEntityToResponse(player)); + + deletePlayer(player); + } + + @Test + @SneakyThrows + void getPlayerById_shouldThrowPlayerNotFoundException_whenPlayerNotFound() { + mockMvc.perform(get(URI + "/" + BAD_PLAYER_ID) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isNotFound(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.id").value(BAD_TEAM_ID), + jsonPath("$.message").value("id.not_found"), + jsonPath("$.error").value("Not Found"), + jsonPath("$.status").value(404) + ); + } + + @Test + @SneakyThrows + @Transactional + public void addPlayerToTeam_success() { + + User user = createUser("addPlayerToTeam_success"); + user = userRepository.save(user); + + Group group = createGroup(user); + group = groupRepository.save(group); + + Team team = createTeam(group); + team = teamRepository.save(team); + + var payload = createPlayerPayload(); + + when(ratingCache.get(payload.login())).thenReturn(0.); + + var mvcResponse = mockMvc.perform(post(URI + "/team/" + team.getId()) + .header("Authorization", userBearerToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + var response = objectMapper.readValue(mvcResponse.getContentAsString(), PlayerResponse.class); + var playerFromDb = playerRepository.findById(response.id()); + + assertAll( + () -> assertThat(playerFromDb).isPresent(), + () -> assertThat(response.login()).isEqualTo(payload.login()), + () -> assertThat(response.rating()).isEqualTo(0.), + () -> assertThat(playerFromDb.get().getTeams()).hasSize(1) + ); + + deletePlayer(playerFromDb.get()); + deleteTeamFromDb(team); + deleteGroupFromDb(group); + deleteUserFromDb(user); + } + + @Test + @SneakyThrows + void addPlayerToTeam_shouldThrowTeamNotFoundException_whenTeamNotFound() { + var payload = createPlayerPayload(); + + mockMvc.perform(post(URI + "/team/" + BAD_TEAM_ID) + .header("Authorization", userBearerToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpectAll( + status().isNotFound(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.id").value(BAD_TEAM_ID), + jsonPath("$.message").value("id.not_found"), + jsonPath("$.error").value("Not Found"), + jsonPath("$.status").value(404) + ); + } + + @Test + @SneakyThrows + void addPlayerToTeam_shouldThrowPlayerAlreadyInTeamException_whenPlayerAlreadyInTeam() { + User user = createUser("addPlayerToTeam_shouldThrowPlayerAlreadyInTeamException_whenPlayerAlreadyInTeam"); + user = userRepository.save(user); + + Group group = createGroup(user); + group = groupRepository.save(group); + + Team team = createTeam(group); + team = teamRepository.save(team); + + Player player = createPlayer(); + player = playerRepository.save(player); + + team.setPlayers(List.of(player)); + team = teamRepository.save(team); + + var payload = createPlayerPayload(); + + mockMvc.perform(post(URI + "/team/" + team.getId()) + .header("Authorization", userBearerToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpectAll( + status().isConflict(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.login").value(player.getLogin()), + jsonPath("$.message").value("player.already_in_team"), + jsonPath("$.error").value("Conflict"), + jsonPath("$.status").value(409) + ); + + deletePlayer(player); + deleteTeamFromDb(team); + deleteGroupFromDb(group); + deleteUserFromDb(user); + } + + @Test + @SneakyThrows + @Transactional + public void updatePlayerInTeam_success() { + User user = createUser("updatePlayerInTeam_success"); + user = userRepository.save(user); + + Group group = createGroup(user); + group = groupRepository.save(group); + + Team team = createTeam(group); + team = teamRepository.save(team); + + Player player = createPlayer(); + player = playerRepository.save(player); + + team.setPlayers(new ArrayList<>(List.of(player))); + team = teamRepository.save(team); + + var payload = createPlayerPayload("awoo"); + + when(ratingCache.get(payload.login())).thenReturn(0.); + + var mvcResponse = mockMvc.perform(put(URI + "/players/" + player.getId() + + "/teams/" + team.getId()) + .header("Authorization", userBearerToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + + var response = objectMapper.readValue(mvcResponse.getContentAsString(), PlayerResponse.class); + var playerFromDb = playerRepository.findById(response.id()); + + assertAll( + () -> assertThat(playerFromDb).isPresent(), + () -> assertThat(response.login()).isEqualTo(payload.login()), + () -> assertThat(response.rating()).isEqualTo(0.), + () -> assertThat(playerFromDb.get().getTeams()).hasSize(1) + ); + + deletePlayer(player); + deletePlayer(playerFromDb.get()); + deleteTeamFromDb(team); + deleteGroupFromDb(group); + deleteUserFromDb(user); + } + + @Test + @SneakyThrows + void updatePlayerInTeam_shouldThrowPlayerNotFoundException_whenPlayerNotFound() { + var payload = createPlayerPayload(); + + var mvcResponse = mockMvc.perform(put(URI + "/players/" + BAD_PLAYER_ID + + "/teams/" + 1L) + .header("Authorization", userBearerToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpectAll( + status().isNotFound(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.id").value(BAD_PLAYER_ID), + jsonPath("$.message").value("id.not_found"), + jsonPath("$.error").value("Not Found"), + jsonPath("$.status").value(404) + ); + } + + + @Test + @SneakyThrows + void updatePlayerInTeam_shouldThrowTeamNotFoundException_whenTeamNotFound() { + Player player = createPlayer(); + player = playerRepository.save(player); + + var payload = createPlayerPayload(); + + var mvcResponse = mockMvc.perform(put(URI + "/players/" + player.getId() + + "/teams/" + BAD_TEAM_ID) + .header("Authorization", userBearerToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpectAll( + status().isNotFound(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.id").value(BAD_TEAM_ID), + jsonPath("$.message").value("id.not_found"), + jsonPath("$.error").value("Not Found"), + jsonPath("$.status").value(404) + ); + + deletePlayer(player); + } + + @Test + @SneakyThrows + void updatePlayerInTeam_shouldThrowPlayerNotFromTeamException_whenPlayerNotFromTeam() { + User user = createUser("updatePlayerInTeam_shouldThrowPlayerNotFromTeamException_whenPlayerNotFromTeam"); + user = userRepository.save(user); + + Group group = createGroup(user); + group = groupRepository.save(group); + + Team team = createTeam(group); + team = teamRepository.save(team); + + Player player = createPlayer(); + player = playerRepository.save(player); + + var payload = createPlayerPayload(); + + var mvcResponse = mockMvc.perform(put(URI + "/players/" + player.getId() + + "/teams/" + team.getId()) + .header("Authorization", userBearerToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpectAll( + status().isConflict(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.playerId").value(player.getId()), + jsonPath("$.details.teamId").value(team.getId()), + jsonPath("$.message").value("player.not_from_team"), + jsonPath("$.error").value("Conflict"), + jsonPath("$.status").value(409) + ); + + deletePlayer(player); + deleteTeamFromDb(team); + deleteGroupFromDb(group); + deleteUserFromDb(user); + } + + @Test + @SneakyThrows + @Transactional + public void deletePlayerFromTeam_success() { + User user = createUser("deletePlayerFromTeam"); + user = userRepository.save(user); + + Group group = createGroup(user); + group = groupRepository.save(group); + + Team team = createTeam(group); + team = teamRepository.save(team); + + Player player = createPlayer(); + player = playerRepository.save(player); + + team.setPlayers(new ArrayList<>(List.of(player))); + team = teamRepository.save(team); + + mockMvc.perform(delete(URI + "/players/" + player.getId() + + "/teams/" + team.getId()) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isNoContent(), + content().string("") + ); + + var teamFromDb = teamRepository.findById(team.getId()); + var playerFromDb = playerRepository.findById(player.getId()); + + assertAll( + () -> assertThat(teamFromDb).isPresent(), + () -> assertThat(teamFromDb.get().getPlayers()).isEmpty(), + () -> assertThat(playerFromDb).isPresent(), + () -> assertThat(playerFromDb.get().getTeams()).isEmpty() + ); + + deletePlayer(player); + deleteTeamFromDb(team); + deleteGroupFromDb(group); + deleteUserFromDb(user); + } + + @Test + @SneakyThrows + void deletePlayerFromTeam_shouldThrowPlayerNotFoundException_whenPlayerNotFound() { + mockMvc.perform(delete(URI + "/players/" + BAD_PLAYER_ID+ + "/teams/" + 1L) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isNotFound(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.id").value(BAD_TEAM_ID), + jsonPath("$.message").value("id.not_found"), + jsonPath("$.error").value("Not Found"), + jsonPath("$.status").value(404) + ); + } + + @Test + @SneakyThrows + void deletePlayerFromTeam_shouldThrowTeamNotFoundException_whenTeamNotFound() { + Player player = createPlayer(); + player = playerRepository.save(player); + + + mockMvc.perform(delete(URI + "/players/" + BAD_PLAYER_ID+ + "/teams/" + BAD_TEAM_ID) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isNotFound(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.id").value(BAD_TEAM_ID), + jsonPath("$.message").value("id.not_found"), + jsonPath("$.error").value("Not Found"), + jsonPath("$.status").value(404) + ); + + deletePlayer(player); + } + + @Test + @SneakyThrows + @Transactional + public void deleteAllPlayersFromTeam_success() { + User user = createUser("deleteAllTeamsByGroup_success"); + user = userRepository.save(user); + + Group group = createGroup(user); + group = groupRepository.save(group); + + Team team = createTeam(group); + + Player player1 = createPlayer("p1"); + Player player2 = createPlayer("p2"); + + player1.setTeams(new ArrayList<>(List.of(team))); + player2.setTeams(new ArrayList<>(List.of(team))); + + player1 = playerRepository.save(player1); + player2 = playerRepository.save(player2); + + mockMvc.perform(delete(URI + "/team/" + team.getId()) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isNoContent(), + content().string("") + ); + + var teamFromDb = teamRepository.findById(team.getId()); + var playerFromDb1 = playerRepository.findById(player1.getId()); + var playerFromDb2 = playerRepository.findById(player2.getId()); + + assertAll( + () -> assertThat(teamFromDb).isPresent(), + () -> assertThat(playerFromDb1).isPresent(), + () -> assertThat(playerFromDb2).isPresent(), + () -> assertThat(teamFromDb.get().getPlayers()).isEmpty(), + () -> assertThat(playerFromDb1.get().getTeams()).hasSize(1), + () -> assertThat(playerFromDb2.get().getTeams()).hasSize(1) + ); + + var mvcResponse = mockMvc.perform(get(URI + "/team/" + team.getId()) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + var players = objectMapper.readValue( + mvcResponse.getContentAsString(), + new TypeReference>() { + } + ); + + assertThat(players).isEmpty(); + + deletePlayer(player1); + deletePlayer(player2); + deleteTeamFromDb(team); + deleteGroupFromDb(group); + deleteUserFromDb(user);; + } + + @Test + @SneakyThrows + void deleteAllPlayersFromTeam_shouldThrowTeamNotFoundException_whenTeamNotFound() { + mockMvc.perform(delete(URI + "/team/" + BAD_TEAM_ID) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isNotFound(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.id").value(BAD_TEAM_ID), + jsonPath("$.message").value("id.not_found"), + jsonPath("$.error").value("Not Found"), + jsonPath("$.status").value(404) + ); + } + + + private PlayerPayload createPlayerPayload() { + return PlayerPayload.builder() + .login("tourist") + .build(); + } + + private PlayerPayload createPlayerPayload(String login) { + return PlayerPayload.builder() + .login(login) + .build(); + } + + private User createUser(String login) { + return User.builder() + .name("Team controller User name") + .login(login) + .hashedPassword("Password") + .role(Role.USER) + .build(); + } + + private Group createGroup(User user) { + return Group.builder() + .name("Test Group") + .description("Test description") + .user(user) + .build(); + } + + private Team createTeam(Group group) { + return Team.builder() + .name("team") + .description("team description") + .group(group) + .build(); + } + + private Player createPlayer() { + return Player.builder() + .login("tourist") + .build(); + } + + private Player createPlayer(String login) { + return Player.builder() + .login(login) + .build(); + } + + private void deleteTeamFromDb(Team team) { + teamRepository.delete(team); + } + + private void deleteGroupFromDb(Group group) { + groupRepository.delete(group); + } + + private void deleteUserFromDb(User user) { + userRepository.delete(user); + } + + private void deletePlayer(Player player) { + for (Team team : player.getTeams()) { + team.getPlayers().remove(player); + } + + var teams = List.copyOf(player.getTeams()); + player.setTeams(List.of()); + + teamRepository.saveAll(teams); + playerRepository.delete(player); + } +} \ No newline at end of file diff --git a/src/test/java/com/cf/cfteam/controllers/codeforces/integration/TeamControllerIntegrationTest.java b/src/test/java/com/cf/cfteam/controllers/codeforces/integration/TeamControllerIntegrationTest.java new file mode 100644 index 0000000..41fcabb --- /dev/null +++ b/src/test/java/com/cf/cfteam/controllers/codeforces/integration/TeamControllerIntegrationTest.java @@ -0,0 +1,378 @@ +package com.cf.cfteam.controllers.codeforces.integration; + +import com.cf.cfteam.models.entities.codeforces.Team; +import com.cf.cfteam.repositories.jpa.codeforces.TeamRepository; +import com.cf.cfteam.transfer.payloads.codeforces.TeamPayload; +import com.cf.cfteam.transfer.responses.codeforces.TeamResponse; +import com.cf.cfteam.utils.codeforces.mappers.TeamMapper; +import com.fasterxml.jackson.core.type.TypeReference; +import com.cf.cfteam.BaseIntegrationTest; +import com.cf.cfteam.models.entities.codeforces.Group; +import com.cf.cfteam.models.entities.security.Role; +import com.cf.cfteam.models.entities.security.User; +import com.cf.cfteam.repositories.jpa.codeforces.GroupRepository; +import com.cf.cfteam.repositories.jpa.security.UserRepository; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + + +@ActiveProfiles("test") +public class TeamControllerIntegrationTest extends BaseIntegrationTest { + + private static final String URI = "/api/cf/teams"; + + private static final Long BAD_TEAM_ID = 9999L; + private static final Long BAD_GROUP_ID = 9999L; + + @Autowired + private TeamRepository teamRepository; + + @Autowired + private GroupRepository groupRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private TeamMapper teamMapper; + + @Test + @SneakyThrows + public void getAllGroupsByUser_notEmpty_success() { + User user = createUser("getAllGroupsByUser_notEmpty_success"); + user = userRepository.save(user); + + Group group = createGroup(user); + group = groupRepository.save(group); + + Team team = createTeam(group); + team = teamRepository.save(team); + + var mvcResponse = mockMvc.perform(get(URI + "/group/" + group.getId()) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + var teams = objectMapper.readValue( + mvcResponse.getContentAsString(), + new TypeReference>() { + } + ); + + assertThat(teams).hasSize(1) + .contains(teamMapper.fromEntityToResponse(team)); + + deleteTeamFromDb(team); + deleteGroupFromDb(group); + } + + @Test + @SneakyThrows + void getAllTeamsByGroup_shouldThrowGroupNotFoundException_whenGroupNotFound() { + mockMvc.perform(get(URI + "/group/" + BAD_GROUP_ID) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isNotFound(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.id").value(BAD_GROUP_ID), + jsonPath("$.message").value("id.not_found"), + jsonPath("$.error").value("Not Found"), + jsonPath("$.status").value(404) + ); + } + + @Test + @SneakyThrows + public void getTeamById_notEmpty() { + User user = createUser("getTeamById_notEmpty"); + user = userRepository.save(user); + + Group group = createGroup(user); + group = groupRepository.save(group); + + Team team = createTeam(group); + team = teamRepository.save(team); + + var mvcResponse = mockMvc.perform(get(URI + "/" + team.getId()) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + var response = objectMapper.readValue(mvcResponse.getContentAsString(), TeamResponse.class); + + assertThat(response).isEqualTo(teamMapper.fromEntityToResponse(team)); + + deleteTeamFromDb(team); + deleteGroupFromDb(group); + } + + @Test + @SneakyThrows + void getTeamById_shouldThrowTeamNotFoundException_whenTeamNotFoundE() { + mockMvc.perform(get(URI + "/" + BAD_TEAM_ID) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isNotFound(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.id").value(BAD_TEAM_ID), + jsonPath("$.message").value("id.not_found"), + jsonPath("$.error").value("Not Found"), + jsonPath("$.status").value(404) + ); + } + + @Test + @SneakyThrows + public void addTeamToGroup_success() { + User user = createUser("addTeamToGroup_success"); + user = userRepository.save(user); + + Group group = createGroup(user); + group = groupRepository.save(group); + + var payload = createTeamPayload(); + + var mvcResponse = mockMvc.perform(post(URI + "/group/" + group.getId()) + .header("Authorization", userBearerToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + var response = objectMapper.readValue(mvcResponse.getContentAsString(), TeamResponse.class); + var teamFromDb = teamRepository.findById(response.id()); + + assertAll( + () -> assertThat(teamFromDb).isPresent(), + () -> assertThat(response.description()).isEqualTo(payload.description()), + () -> assertThat(response.name()).isEqualTo(payload.name()) + ); + + deleteTeamFromDb(teamFromDb.get()); + deleteGroupFromDb(group); + } + + @Test + @SneakyThrows + void addTeamToGroup_shouldThrowGroupNotFoundException_whenGroupNotFound() { + var payload = createTeamPayload(); + + mockMvc.perform(post(URI + "/group/" + BAD_GROUP_ID) + .header("Authorization", userBearerToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpectAll( + status().isNotFound(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.id").value(BAD_GROUP_ID), + jsonPath("$.message").value("id.not_found"), + jsonPath("$.error").value("Not Found"), + jsonPath("$.status").value(404) + ); + } + + @Test + @SneakyThrows + public void updateTeam_success() { + User user = createUser("updateTeam_success"); + user = userRepository.save(user); + + Group group = createGroup(user); + group = groupRepository.save(group); + + var payload = createTeamPayload("new name", "new description"); + + Team team = createTeam(group); + team = teamRepository.save(team); + + + var mvcResponse = mockMvc.perform(put(URI + "/" + team.getId()) + .header("Authorization", userBearerToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + + var response = objectMapper.readValue(mvcResponse.getContentAsString(), TeamResponse.class); + var teamFromDb = teamRepository.findById(response.id()); + + assertAll( + () -> assertThat(teamFromDb).isPresent(), + () -> assertThat(teamFromDb.get().getDescription()).isEqualTo(payload.description()), + () -> assertThat(teamFromDb.get().getName()).isEqualTo(payload.name()) + ); + + deleteTeamFromDb(teamFromDb.get()); + deleteGroupFromDb(group); + } + + @Test + @SneakyThrows + void updateTeam_shouldThrowTeamNotFoundException_whenTeamNotFound() { + var payload = createTeamPayload(); + + mockMvc.perform(put(URI + "/" + BAD_TEAM_ID) + .header("Authorization", userBearerToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpectAll( + status().isNotFound(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.details.id").value(BAD_TEAM_ID), + jsonPath("$.message").value("id.not_found"), + jsonPath("$.error").value("Not Found"), + jsonPath("$.status").value(404) + ); + } + + @Test + @SneakyThrows + public void deleteTeam_success() { + User user = createUser("deleteTeam_success"); + user = userRepository.save(user); + + Group group = createGroup(user); + group = groupRepository.save(group); + + Team team = createTeam(group); + team = teamRepository.save(team); + + mockMvc.perform(delete(URI + "/" + team.getId()) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isNoContent(), + content().string("") + ); + + var teamFromDb = teamRepository.findById(team.getId()); + + assertThat(teamFromDb).isNotPresent(); + + deleteGroupFromDb(group); + } + + @Test + @SneakyThrows + public void deleteAllTeamsByGroup_success() { + User user = createUser("deleteAllTeamsByGroup_success"); + user = userRepository.save(user); + + Group group = createGroup(user); + group = groupRepository.save(group); + + Team team1 = createTeam(group); + team1 = teamRepository.save(team1); + + Team team2 = createTeam(group); + team2 = teamRepository.save(team2); + + mockMvc.perform(delete(URI + "/group/" + group.getId()) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isNoContent(), + content().string("") + ); + + var teamFromDb1 = teamRepository.findById(team1.getId()); + var teamFromDb2 = teamRepository.findById(team2.getId()); + + assertAll( + () -> assertThat(teamFromDb1).isNotPresent(), + () -> assertThat(teamFromDb2).isNotPresent() + ); + + var mvcResponse = mockMvc.perform(get(URI + "/group/" + group.getId()) + .header("Authorization", userBearerToken)) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + var teams = objectMapper.readValue( + mvcResponse.getContentAsString(), + new TypeReference>() { + } + ); + + assertThat(teams).isEmpty(); + + deleteGroupFromDb(group); + } + + + private TeamPayload createTeamPayload() { + return TeamPayload.builder() + .name("name") + .description("description") + .build(); + } + + private TeamPayload createTeamPayload(String name, String description) { + return TeamPayload.builder() + .name(name) + .description(description) + .build(); + } + + private User createUser(String login) { + return User.builder() + .name("Team controller User name") + .login(login) + .hashedPassword("Password") + .role(Role.USER) + .build(); + } + + private Group createGroup(User user) { + return Group.builder() + .name("Test Group") + .description("Test description") + .user(user) + .build(); + } + + private Team createTeam(Group group) { + return Team.builder() + .name("team") + .description("team description") + .group(group) + .build(); + } + + + private void deleteTeamFromDb(Team team) { + teamRepository.delete(team); + } + + private void deleteGroupFromDb(Group group) { + groupRepository.delete(group); + } + + private void deleteUserFromDb(User user) { + userRepository.delete(user); + } +} diff --git a/src/test/java/com/cf/cfteam/controllers/codeforces/unit/GroupControllerTest.java b/src/test/java/com/cf/cfteam/controllers/codeforces/unit/GroupControllerTest.java new file mode 100644 index 0000000..8199621 --- /dev/null +++ b/src/test/java/com/cf/cfteam/controllers/codeforces/unit/GroupControllerTest.java @@ -0,0 +1,112 @@ +package com.cf.cfteam.controllers.codeforces.unit; + +import com.cf.cfteam.controllers.codeforces.GroupController; +import com.cf.cfteam.services.codeforces.GroupService; +import com.cf.cfteam.transfer.payloads.codeforces.GroupPayload; +import com.cf.cfteam.transfer.responses.codeforces.GroupResponse; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.*; + + +@ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) +class GroupControllerTest { + + @InjectMocks + private GroupController groupController; + + @Mock + private GroupService groupService; + + private static GroupPayload groupPayload; + private static GroupResponse groupResponse; + + @BeforeAll + static void setUp() { + groupPayload = GroupPayload.builder() + .name("Test Group") + .description("Test description") + .build(); + + groupResponse = GroupResponse.builder() + .name("Test Group") + .description("Test description") + .teams(null) + .build(); + } + + @Test + void shouldReturnGroupsByUserId() { + when(groupService.getAllGroupsByUser(1L)).thenReturn(List.of(groupResponse)); + + List groups = groupController.getAllGroupsByUser(1L, null).getBody(); + + verify(groupService, times(1)).getAllGroupsByUser(1L); + + assertThat(groups) + .isNotNull() + .hasSize(1) + .contains(groupResponse); + } + + @Test + void shouldReturnGroupById() { + when(groupService.getGroupById(1L)).thenReturn(groupResponse); + + GroupResponse result = groupController.getGroupById(1L, null).getBody(); + + verify(groupService, times(1)).getGroupById(1L); + assertThat(result).isEqualTo(groupResponse); + } + + @Test + void shouldAddGroupToUser() { + when(groupService.addGroupToUser(1L, groupPayload)).thenReturn(groupResponse); + + GroupResponse result = groupController.addGroupToUser(1L, groupPayload, null).getBody(); + + verify(groupService, times(1)).addGroupToUser(1L, groupPayload); + + assertThat(result).isEqualTo(groupResponse); + } + + @Test + void shouldUpdateGroup() { + when(groupService.updateGroup(1L, groupPayload)).thenReturn(groupResponse); + + GroupResponse result = groupController.updateGroup(1L, groupPayload, null).getBody(); + + verify(groupService, times(1)).updateGroup(1L, groupPayload); + assertAll( + () -> assertThat(result).isNotNull(), + () -> assertThat(result.name()).isEqualTo(groupPayload.name()), + () -> assertThat(result.description()).isEqualTo(groupPayload.description()) + ); + } + + @Test + void shouldDeleteGroup() { + groupController.deleteGroup(1L, null); + + verify(groupService, times(1)).deleteGroup(1L); + } + + @Test + void shouldDeleteAllGroupsByUser() { + groupController.deleteAllGroupsByUser(1L, null); + + verify(groupService, times(1)).deleteAllGroupsByUser(1L); + } +} + diff --git a/src/test/java/com/cf/cfteam/controllers/codeforces/unit/PlayerControllerTest.java b/src/test/java/com/cf/cfteam/controllers/codeforces/unit/PlayerControllerTest.java new file mode 100644 index 0000000..df33038 --- /dev/null +++ b/src/test/java/com/cf/cfteam/controllers/codeforces/unit/PlayerControllerTest.java @@ -0,0 +1,109 @@ +package com.cf.cfteam.controllers.codeforces.unit; + +import com.cf.cfteam.controllers.codeforces.PlayerController; +import com.cf.cfteam.services.codeforces.PlayerService; +import com.cf.cfteam.transfer.payloads.codeforces.PlayerPayload; +import com.cf.cfteam.transfer.responses.codeforces.PlayerResponse; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.*; + + +@ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) +class PlayerControllerTest { + + @InjectMocks + private PlayerController playerController; + + @Mock + private PlayerService playerService; + + private static PlayerPayload playerPayload; + private static PlayerResponse playerResponse; + + @BeforeAll + static void setUp() { + playerPayload = PlayerPayload.builder() + .login("login") + .build(); + + playerResponse = PlayerResponse.builder() + .id(1L) + .rating(1000.) + .login("login") + .build(); + } + + @Test + void shouldReturnAllPlayersByTeam() { + when(playerService.getAllPlayersByTeam(1L)).thenReturn(List.of(playerResponse)); + + var players = playerController.getAllPlayersByTeam(1L, null).getBody(); + + verify(playerService, times(1)).getAllPlayersByTeam(1L); + + assertThat(players) + .isNotNull() + .hasSize(1) + .contains(playerResponse); + } + + @Test + void shouldReturnPlayerById() { + when(playerService.getPlayerById(1L)).thenReturn(playerResponse); + + var result = playerController.getPlayerById(1L, null).getBody(); + + verify(playerService, times(1)).getPlayerById(1L); + assertThat(result).isEqualTo(playerResponse); + } + + @Test + void shouldAddPlayerToTeam() { + when(playerService.addPlayerToTeam(1L, playerPayload)).thenReturn(playerResponse); + + var result = playerController.addPlayerToTeam(1L, playerPayload, null).getBody(); + + verify(playerService, times(1)).addPlayerToTeam(1L, playerPayload); + + assertThat(result).isEqualTo(playerResponse); + } + + @Test + void shouldUpdatePlayerInTeam() { + when(playerService.updatePlayerInTeam(1L, 1L, playerPayload)).thenReturn(playerResponse); + + var result = playerController.updatePlayerInTeam(1L, 1L, playerPayload, null).getBody(); + + verify(playerService, times(1)).updatePlayerInTeam(1L, 1L, playerPayload); + assertAll( + () -> assertThat(result).isNotNull(), + () -> assertThat(result.login()).isEqualTo(playerResponse.login()) + ); + } + + @Test + void shouldDeletePlayerFromTeam() { + playerController.deletePlayerFromTeam(1L, 1L, null); + + verify(playerService, times(1)).deletePlayerFromTeam(1L, 1L); + } + + @Test + void shouldDeleteAllPlayersFromTeam() { + playerController.deleteAllPlayersFromTeam(1L, null); + + verify(playerService, times(1)).deleteAllPlayersFromTeam(1L); + } +} \ No newline at end of file diff --git a/src/test/java/com/cf/cfteam/controllers/codeforces/unit/TeamControllerTest.java b/src/test/java/com/cf/cfteam/controllers/codeforces/unit/TeamControllerTest.java new file mode 100644 index 0000000..4438f2c --- /dev/null +++ b/src/test/java/com/cf/cfteam/controllers/codeforces/unit/TeamControllerTest.java @@ -0,0 +1,113 @@ +package com.cf.cfteam.controllers.codeforces.unit; + +import com.cf.cfteam.controllers.codeforces.TeamController; +import com.cf.cfteam.services.codeforces.TeamService; +import com.cf.cfteam.transfer.payloads.codeforces.TeamPayload; +import com.cf.cfteam.transfer.responses.codeforces.TeamResponse; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.*; + + +@ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) +class TeamControllerTest { + + @InjectMocks + private TeamController teamController; + + @Mock + private TeamService teamService; + + private static TeamPayload teamPayload; + private static TeamResponse teamResponse; + + @BeforeAll + static void setUp() { + teamPayload = TeamPayload.builder() + .name("team name") + .description("team description") + .build(); + + teamResponse = TeamResponse.builder() + .id(1L) + .name("team name") + .description("team description") + .players(List.of()) + .teamRating(0.) + .build(); + } + + @Test + void shouldReturnAllTeamsByGroup() { + when(teamService.getAllTeamsByGroup(1L)).thenReturn(List.of(teamResponse)); + + var teams = teamController.getAllTeamsByGroup(1L, null).getBody(); + + verify(teamService, times(1)).getAllTeamsByGroup(1L); + + assertThat(teams) + .isNotNull() + .hasSize(1) + .contains(teamResponse); + } + + @Test + void shouldReturnTeamById() { + when(teamService.getTeamById(1L)).thenReturn(teamResponse); + + var result = teamController.getTeamById(1L, null).getBody(); + + verify(teamService, times(1)).getTeamById(1L); + assertThat(result).isEqualTo(teamResponse); + } + + @Test + void shouldAddTeamToGroup() { + when(teamService.addTeamToGroup(1L, teamPayload)).thenReturn(teamResponse); + + var result = teamController.addTeamToGroup(1L, teamPayload, null).getBody(); + + verify(teamService, times(1)).addTeamToGroup(1L, teamPayload); + + assertThat(result).isEqualTo(teamResponse); + } + + @Test + void shouldUpdateTeam() { + when(teamService.updateTeam(1L, teamPayload)).thenReturn(teamResponse); + + var result = teamController.updateTeam(1L, teamPayload, null).getBody(); + + verify(teamService, times(1)).updateTeam(1L, teamPayload); + assertAll( + () -> assertThat(result).isNotNull(), + () -> assertThat(result.name()).isEqualTo(teamResponse.name()), + () -> assertThat(result.description()).isEqualTo(teamResponse.description()) + ); + } + + @Test + void shouldDeleteGroup() { + teamController.deleteTeam(1L, null); + + verify(teamService, times(1)).deleteTeam(1L); + } + + @Test + void shouldDeleteAllGroupsByUser() { + teamController.deleteAllTeamsByGroup(1L, null); + + verify(teamService, times(1)).deleteAllTeamsByGroup(1L); + } +} \ No newline at end of file diff --git a/src/test/java/com/cf/cfteam/controllers/security/UserControllerIntegrationTest.java b/src/test/java/com/cf/cfteam/controllers/security/UserControllerIntegrationTest.java index 31db666..267bf57 100644 --- a/src/test/java/com/cf/cfteam/controllers/security/UserControllerIntegrationTest.java +++ b/src/test/java/com/cf/cfteam/controllers/security/UserControllerIntegrationTest.java @@ -8,6 +8,7 @@ import com.cf.cfteam.transfer.payloads.security.ChangePasswordPayload; import com.cf.cfteam.transfer.payloads.security.RegistrationPayload; import com.cf.cfteam.transfer.responses.security.JwtAuthenticationResponse; +import lombok.SneakyThrows; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; @@ -32,7 +33,8 @@ public class UserControllerIntegrationTest extends BaseIntegrationTest { private TokenRepository tokenRepository; @Test - public void register_success() throws Exception { + @SneakyThrows + public void register_success() { RegistrationPayload payload = RegistrationPayload.builder() .login("register-login") .name("register-name") @@ -57,7 +59,8 @@ public void register_success() throws Exception { } @Test - public void register_shouldThrowUserAlreadyRegisterException_WhenUserAlreadyRegistered() throws Exception { + @SneakyThrows + public void register_shouldThrowUserAlreadyRegisterException_WhenUserAlreadyRegistered() { String login = "user"; String password = "password"; String name = "Test User"; @@ -82,7 +85,8 @@ public void register_shouldThrowUserAlreadyRegisterException_WhenUserAlreadyRegi } @Test - public void login_success() throws Exception { + @SneakyThrows + public void login_success() { RegistrationPayload payloadToRegister = RegistrationPayload.builder() .login("register-login") .name("register-name") @@ -130,7 +134,8 @@ public void login_success() throws Exception { } @Test - public void logout_success() throws Exception { + @SneakyThrows + public void logout_success() { RegistrationPayload payloadToRegister = RegistrationPayload.builder() .login("register-login") .name("register-name") @@ -157,7 +162,8 @@ public void logout_success() throws Exception { } @Test - public void changePassword_success() throws Exception { + @SneakyThrows + public void changePassword_success() { RegistrationPayload payloadToRegister = RegistrationPayload.builder() .login("register-login") .name("register-name") @@ -232,7 +238,8 @@ public void changePassword_success() throws Exception { } @Test - public void changePassword_shouldReturnBadRequest_WhenTwoFactorCodeInvalid() throws Exception { + @SneakyThrows + public void changePassword_shouldReturnBadRequest_WhenTwoFactorCodeInvalid() { RegistrationPayload payloadToRegister = RegistrationPayload.builder() .login("register-login") .name("register-name") @@ -262,8 +269,8 @@ public void changePassword_shouldReturnBadRequest_WhenTwoFactorCodeInvalid() thr deleteUserFromDb(user.get()); } - - private JwtAuthenticationResponse register(RegistrationPayload payload) throws Exception { + @SneakyThrows + private JwtAuthenticationResponse register(RegistrationPayload payload) { var response = mockMvc.perform(post(uri + "/register") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(payload))) diff --git a/src/test/java/com/cf/cfteam/controllers/security/UserControllerTest.java b/src/test/java/com/cf/cfteam/controllers/security/UserControllerTest.java index fea4bc8..65c737c 100644 --- a/src/test/java/com/cf/cfteam/controllers/security/UserControllerTest.java +++ b/src/test/java/com/cf/cfteam/controllers/security/UserControllerTest.java @@ -8,9 +8,11 @@ import com.cf.cfteam.transfer.responses.security.JwtAuthenticationResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.context.ActiveProfiles; import org.springframework.security.core.Authentication; @@ -19,6 +21,7 @@ import static org.mockito.Mockito.when; @ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) class UserControllerTest { @InjectMocks @@ -30,11 +33,6 @@ class UserControllerTest { @Mock private Authentication authentication; - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - } - @Test void testRegister_success() { RegistrationPayload registrationPayload = getRegistrationPayload(); diff --git a/src/test/java/com/cf/cfteam/services/client/CodeforcesClientTest.java b/src/test/java/com/cf/cfteam/services/client/CodeforcesClientTest.java new file mode 100644 index 0000000..1cd940e --- /dev/null +++ b/src/test/java/com/cf/cfteam/services/client/CodeforcesClientTest.java @@ -0,0 +1,53 @@ +package com.cf.cfteam.services.client; + +import com.cf.cfteam.BaseIntegrationTest; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import java.nio.file.Files; +import java.nio.file.Paths; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.assertThat; + +@ActiveProfiles("test") +@SpringBootTest +@WireMockTest(httpPort = 8080) +class CodeforcesClientTest extends BaseIntegrationTest { + + @Autowired + private CodeforcesClient codeforcesClient; + + private static final String MOCK_JSON_PATH = "src/test/resources/codeforces/responses/"; + + + @DynamicPropertySource + static void setProperties(DynamicPropertyRegistry registry) { + registry.add("codeforces.api.base.url", () -> "http://localhost:8080/api"); + } + + @Test + @SneakyThrows + void fetchRatingFromApi_ShouldReturnRating() { + String responseBody = Files.readString(Paths.get(MOCK_JSON_PATH + "tourist.json")); + + stubFor(get(urlPathEqualTo("/api/user.info")) + .withQueryParam("handles", equalTo("tourist")) + .withQueryParam("checkHistoricHandles", equalTo("false")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .withBody(responseBody))); + + Double rating = codeforcesClient.fetchRatingFromApi("tourist"); + + assertThat(rating).isEqualTo(4009); + } +} \ No newline at end of file diff --git a/src/test/java/com/cf/cfteam/services/codeforces/GroupServiceTest.java b/src/test/java/com/cf/cfteam/services/codeforces/GroupServiceTest.java new file mode 100644 index 0000000..e8fbb1a --- /dev/null +++ b/src/test/java/com/cf/cfteam/services/codeforces/GroupServiceTest.java @@ -0,0 +1,191 @@ +package com.cf.cfteam.services.codeforces; + +import com.cf.cfteam.exceptions.codeforces.GroupNotFoundException; +import com.cf.cfteam.exceptions.security.UserNotFoundException; +import com.cf.cfteam.models.entities.codeforces.Group; +import com.cf.cfteam.models.entities.security.Role; +import com.cf.cfteam.models.entities.security.User; +import com.cf.cfteam.repositories.jpa.codeforces.GroupRepository; +import com.cf.cfteam.repositories.jpa.security.UserRepository; +import com.cf.cfteam.transfer.payloads.codeforces.GroupPayload; +import com.cf.cfteam.transfer.responses.codeforces.GroupResponse; +import com.cf.cfteam.utils.codeforces.mappers.GroupMapper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) +class GroupServiceTest { + + @InjectMocks + private GroupService groupService; + + @Mock + private GroupRepository groupRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private GroupMapper groupMapper; + + private static User user; + private static Group group; + private static GroupPayload groupPayload; + private static GroupResponse groupResponse; + + @BeforeAll + static void setUp() { + + user = User.builder() + .name("User name") + .login("User login") + .hashedPassword("Password") + .role(Role.USER) + .build(); + + group = Group.builder() + .name("Test Group") + .description("Test description") + .user(user) + .build(); + + groupPayload = GroupPayload.builder() + .name("Test Group") + .description("Test description") + .build(); + + groupResponse = GroupResponse.builder() + .name("Test Group") + .description("Test description") + .teams(null) + .build(); + } + + @Test + void getAllGroupsByUser_ShouldReturnGroups_WhenUserExists() { + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + when(groupRepository.findByUser(user)).thenReturn(List.of(group)); + when(groupMapper.fromEntityToResponse(group)).thenReturn(groupResponse); + + var groups = groupService.getAllGroupsByUser(1L); + + assertThat(groups).hasSize(1) + .contains(groupResponse); + } + + @Test + void getAllGroupsByUser_ShouldThrowUserNotFoundException_WhenUserDoesNotExist() { + when(userRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> groupService.getAllGroupsByUser(1L)) + .isInstanceOf(UserNotFoundException.class) + .hasMessageContaining("id.not_found"); + } + + @Test + void getGroupById_ShouldReturnGroup_WhenGroupExists() { + when(groupRepository.findById(1L)).thenReturn(Optional.of(group)); + when(groupMapper.fromEntityToResponse(group)).thenReturn(groupResponse); + + var result = groupService.getGroupById(1L); + + assertThat(result).isEqualTo(groupResponse); + } + + @Test + void getGroupById_ShouldThrowGroupNotFoundException_WhenGroupDoesNotExist() { + when(groupRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> groupService.getGroupById(1L)) + .isInstanceOf(GroupNotFoundException.class) + .hasMessageContaining("id.not_found"); + } + + @Test + void addGroupToUser_ShouldAddGroup_WhenUserExists() { + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + when(groupRepository.save(any(Group.class))).thenReturn(group); + when(groupMapper.fromEntityToResponse(group)).thenReturn(groupResponse); + when(groupMapper.fromPayloadToEntity(groupPayload, user)).thenReturn(group); + + var result = groupService.addGroupToUser(1L, groupPayload); + + assertThat(result).isEqualTo(groupResponse); + verify(groupRepository, times(1)).save(any(Group.class)); + } + + @Test + void addGroupToUser_ShouldThrowUserNotFoundException_WhenUserDoesNotExist() { + when(userRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> groupService.addGroupToUser(1L, groupPayload)) + .isInstanceOf(UserNotFoundException.class) + .hasMessageContaining("id.not_found"); + } + + @Test + void updateGroup_ShouldUpdateGroup_WhenGroupExists() { + when(groupRepository.findById(1L)).thenReturn(Optional.of(group)); + when(groupRepository.save(any(Group.class))).thenReturn(group); + when(groupMapper.fromEntityToResponse(group)).thenReturn(groupResponse); + when(groupMapper.updateEntityFromPayload(group, groupPayload)).thenReturn(group); + + var result = groupService.updateGroup(1L, groupPayload); + + assertAll( + () -> assertThat(result.description()).isEqualTo(groupPayload.description()), + () -> assertThat(result.name()).isEqualTo(group.getName()) + ); + verify(groupRepository, times(1)).save(any(Group.class)); + } + + @Test + void updateGroup_ShouldThrowGroupNotFoundException_WhenGroupDoesNotExist() { + when(groupRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> groupService.updateGroup(1L, groupPayload)) + .isInstanceOf(GroupNotFoundException.class) + .hasMessageContaining("id.not_found"); + } + + @Test + void deleteGroup_ShouldDeleteGroup_WhenGroupExists() { + groupService.deleteGroup(1L); + + verify(groupRepository, times(1)).deleteById(1L); + } + + @Test + void deleteAllGroupsByUser_ShouldDeleteAllGroups_WhenUserExists() { + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + when(groupRepository.findByUser(user)).thenReturn(List.of(group)); + + groupService.deleteAllGroupsByUser(1L); + + verify(groupRepository, times(1)).deleteAll(anyList()); + } + + @Test + void deleteAllGroupsByUser_ShouldThrowUserNotFoundException_WhenUserDoesNotExist() { + when(userRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> groupService.deleteAllGroupsByUser(1L)) + .isInstanceOf(UserNotFoundException.class) + .hasMessageContaining("id.not_found"); + } +} \ No newline at end of file diff --git a/src/test/java/com/cf/cfteam/services/codeforces/PlayerServiceTest.java b/src/test/java/com/cf/cfteam/services/codeforces/PlayerServiceTest.java new file mode 100644 index 0000000..3bd64a5 --- /dev/null +++ b/src/test/java/com/cf/cfteam/services/codeforces/PlayerServiceTest.java @@ -0,0 +1,260 @@ +package com.cf.cfteam.services.codeforces; + + +import com.cf.cfteam.exceptions.codeforces.*; +import com.cf.cfteam.models.entities.codeforces.Player; +import com.cf.cfteam.models.entities.codeforces.Team; +import com.cf.cfteam.repositories.jpa.codeforces.PlayerRepository; +import com.cf.cfteam.repositories.jpa.codeforces.TeamRepository; +import com.cf.cfteam.transfer.payloads.codeforces.PlayerPayload; +import com.cf.cfteam.transfer.responses.codeforces.PlayerResponse; +import com.cf.cfteam.utils.codeforces.mappers.PlayerMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +@ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) +class PlayerServiceTest { + + @InjectMocks + private PlayerService playerService; + + @Mock + private PlayerRepository playerRepository; + + @Mock + private TeamRepository teamRepository; + + @Mock + private PlayerMapper playerMapper; + + @Mock + private TeamPlayerLinker teamPlayerLinker; + + private Team team; + private Player player; + private PlayerPayload playerPayload; + private PlayerResponse playerResponse; + + @BeforeEach + void setUp() { + team = Team.builder() + .id(1L) + .name("team") + .description("team description") + .build(); + + player = Player.builder() + .id(1L) + .login("login") + .teams(List.of()) + .build(); + + + playerPayload = PlayerPayload.builder() + .login("login") + .build(); + + playerResponse = PlayerResponse.builder() + .login("login") + .rating(1000.) + .id(1L) + .build(); + } + + + @Test + void getAllPlayersByTeam_ShouldReturnPlayers_WhenTeamExists() { + team.setPlayers(List.of(player)); + when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); + + when(playerMapper.fromEntityToResponse(player)).thenReturn(playerResponse); + + var players = playerService.getAllPlayersByTeam(1L); + + assertThat(players).hasSize(1) + .contains(playerResponse); + } + + @Test + void getAllPlayersByTeam_ShouldThrowTeamNotFoundException_WhenTeamDoesNotExist() { + when(teamRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> playerService.getAllPlayersByTeam(1L)) + .isInstanceOf(TeamNotFoundException.class) + .hasMessageContaining("id.not_found"); + } + + @Test + void getPlayerById_ShouldReturnPlayer_WhenPlayerExists() { + when(playerRepository.findById(1L)).thenReturn(Optional.of(player)); + when(playerMapper.fromEntityToResponse(player)).thenReturn(playerResponse); + + var result = playerService.getPlayerById(1L); + + assertThat(result).isEqualTo(playerResponse); + } + + @Test + void getPlayerById_ShouldThrowPlayerNotFoundException_WhenPlayerDoesNotExist() { + when(playerRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> playerService.getPlayerById(1L)) + .isInstanceOf(PlayerNotFoundException.class) + .hasMessageContaining("id.not_found"); + } + + @Test + void addPlayerToTeam_ShouldAddPlayer_WhenTeamExists() { + when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); + + when(playerRepository.findByLogin("login")).thenReturn(Optional.of(player)); + + doNothing().when(teamPlayerLinker).linkTeamAndPlayer(team, player); + + doReturn(playerResponse).when(playerMapper).fromEntityToResponse(player); + + var result = playerService.addPlayerToTeam(1L, playerPayload); + + assertThat(result).isEqualTo(playerResponse); + verify(teamPlayerLinker, times(1)).linkTeamAndPlayer(team, player); + } + + @Test + void addPlayerToTeam_ShouldThrowTeamNotFoundException_WhenTeamDoesNotExist() { + when(teamRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> playerService.addPlayerToTeam(1L, playerPayload)) + .isInstanceOf(TeamNotFoundException.class) + .hasMessageContaining("id.not_found"); + } + + @Test + void addPlayerToTeam_ShouldThrowPlayerAlreadyInTeamException_WhenPlayerAlreadyInTeamException() { + when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); + when(playerRepository.findByLogin("login")).thenReturn(Optional.of(player)); + + team.setPlayers(List.of(player)); + + assertThatThrownBy(() -> playerService.addPlayerToTeam(1L, playerPayload)) + .isInstanceOf(PlayerAlreadyInTeamException.class) + .hasMessageContaining("player.already_in_team"); + } + + @Test + void updatePlayerInTeam_ShouldUpdatePlayer_WhenTeamAndPlayerExistsAndFromOneTeam() { + team.setPlayers(List.of(player)); + + when(playerRepository.findById(1L)).thenReturn(Optional.of(player)); + when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); + + doNothing().when(teamPlayerLinker).unlinkTeamAndPlayer(team, player); + when(playerRepository.findByLogin("login")).thenReturn(Optional.of(player)); + doNothing().when(teamPlayerLinker).linkTeamAndPlayer(team, player); + + when(playerRepository.findByLogin("login")).thenReturn(Optional.of(player)); + + doReturn(playerResponse).when(playerMapper).fromEntityToResponse(player); + + var result = playerService.updatePlayerInTeam(1L, 1L, playerPayload); + + assertThat(result).isEqualTo(playerResponse); + verify(teamPlayerLinker, times(1)).unlinkTeamAndPlayer(team, player); + verify(teamPlayerLinker, times(1)).linkTeamAndPlayer(team, player); + } + + @Test + void updatePlayerInTeam_ShouldThrowPlayerNotFoundException_WhenPlayerDoesNotExist() { + when(playerRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> playerService.updatePlayerInTeam(1L, 1L, playerPayload)) + .isInstanceOf(PlayerNotFoundException.class) + .hasMessageContaining("id.not_found"); + } + + @Test + void updatePlayerInTeam_ShouldThrowTeamNotFoundException_WhenTeamDoesNotExist() { + when(playerRepository.findById(1L)).thenReturn(Optional.of(player)); + when(teamRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> playerService.updatePlayerInTeam(1L, 1L, playerPayload)) + .isInstanceOf(TeamNotFoundException.class) + .hasMessageContaining("id.not_found"); + } + + @Test + void updatePlayerInTeam_ShouldThrowPlayerNotFromTeamException_WhenPlayerNotFromTeam() { + when(playerRepository.findById(1L)).thenReturn(Optional.of(player)); + when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); + + assertThatThrownBy(() -> playerService.updatePlayerInTeam(1L, 1L, playerPayload)) + .isInstanceOf(PlayerNotFromTeamException.class) + .hasMessageContaining("player.not_from_team"); + } + + @Test + void deletePlayerFromTeam_ShouldDeletePlayerFromTeam_WhenTeamNadPlayerExists() { + when(playerRepository.findById(1L)).thenReturn(Optional.of(player)); + when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); + + doNothing().when(teamPlayerLinker).unlinkTeamAndPlayer(team, player); + playerService.deletePlayerFromTeam(1L, 1L); + verify(teamPlayerLinker, times(1)).unlinkTeamAndPlayer(team, player); + } + + @Test + void deletePlayerFromTeam_ShouldThrowPlayerNotFoundException_WhenPlayerNotFound() { + when(playerRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> playerService.deletePlayerFromTeam(1L, 1L)) + .isInstanceOf(PlayerNotFoundException.class) + .hasMessageContaining("id.not_found"); + } + + @Test + void deletePlayerFromTeam_ShouldThrowTeamNotFoundException_WhenTeamNotFound() { + when(playerRepository.findById(1L)).thenReturn(Optional.of(player)); + when(teamRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> playerService.deletePlayerFromTeam(1L, 1L)) + .isInstanceOf(TeamNotFoundException.class) + .hasMessageContaining("id.not_found"); + } + + @Test + void deleteAllPlayersFromTeam_ShouldDeleteAllPlayersFromTeam_WhenTeamPlayerExists() { + when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); + team.setPlayers(new ArrayList<>(List.of(player))); + player.setTeams(new ArrayList<>(List.of(team))); + + doReturn(team).when(teamRepository).save(team); + doReturn(player).when(playerRepository).save(player); + + playerService.deleteAllPlayersFromTeam(1L); + + verify(teamRepository, times(1)).save(team); + verify(playerRepository, times(1)).save(player); + } + + @Test + void deleteAllPlayersFromTeam_ShouldThrowTeamNotFoundException_WhenTeamNotFound() { + when(teamRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> playerService.deleteAllPlayersFromTeam(1L)) + .isInstanceOf(TeamNotFoundException.class) + .hasMessageContaining("id.not_found"); + } +} \ No newline at end of file diff --git a/src/test/java/com/cf/cfteam/services/codeforces/TeamServiceTest.java b/src/test/java/com/cf/cfteam/services/codeforces/TeamServiceTest.java new file mode 100644 index 0000000..73957c9 --- /dev/null +++ b/src/test/java/com/cf/cfteam/services/codeforces/TeamServiceTest.java @@ -0,0 +1,192 @@ +package com.cf.cfteam.services.codeforces; + +import com.cf.cfteam.exceptions.codeforces.GroupNotFoundException; +import com.cf.cfteam.exceptions.codeforces.TeamNotFoundException; +import com.cf.cfteam.models.entities.codeforces.Group; +import com.cf.cfteam.models.entities.codeforces.Team; +import com.cf.cfteam.repositories.jpa.codeforces.GroupRepository; +import com.cf.cfteam.repositories.jpa.codeforces.TeamRepository; +import com.cf.cfteam.transfer.payloads.codeforces.TeamPayload; +import com.cf.cfteam.transfer.responses.codeforces.TeamResponse; +import com.cf.cfteam.utils.codeforces.mappers.TeamMapper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) +class TeamServiceTest { + + @InjectMocks + private TeamService teamService; + + @Mock + private TeamRepository teamRepository; + + @Mock + private GroupRepository groupRepository; + + @Mock + private TeamMapper teamMapper; + + private static Group group; + private static Team team; + private static TeamPayload teamPayload; + private static TeamResponse teamResponse; + + @BeforeAll + static void setUp() { + group = Group.builder() + .name("Test Group") + .description("Test description") + .user(null) + .build(); + + team = Team.builder() + .name("team") + .description("team description") + .group(group) + .build(); + + teamPayload = TeamPayload.builder() + .name("team") + .description("team description") + .build(); + + teamResponse = TeamResponse.builder() + .name("team") + .description("team description") + .players(List.of()) + .teamRating(0.) + .build(); + } + + @Test + void getAllTeamsByGroup_ShouldReturnTeams_WhenGroupExists() { + when(groupRepository.findById(1L)).thenReturn(Optional.of(group)); + when(teamRepository.findByGroup(group)).thenReturn(List.of(team)); + when(teamMapper.fromEntityToResponse(team)).thenReturn(teamResponse); + + var teams = teamService.getAllTeamsByGroup(1L); + + assertThat(teams).hasSize(1) + .contains(teamResponse); + } + + @Test + void getAllTeamsByGroup_ShouldThrowGroupNotFoundException_WhenGroupDoesNotExist() { + when(groupRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> teamService.getAllTeamsByGroup(1L)) + .isInstanceOf(GroupNotFoundException.class) + .hasMessageContaining("id.not_found"); + } + + @Test + void getTeamById_ShouldReturnTeam_WhenTeamExists() { + when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); + when(teamMapper.fromEntityToResponse(team)).thenReturn(teamResponse); + + var result = teamService.getTeamById(1L); + + assertThat(result).isEqualTo(teamResponse); + } + + @Test + void getTeamById_ShouldThrowTeamNotFoundException_WhenTeamDoesNotExist() { + when(teamRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> teamService.getTeamById(1L)) + .isInstanceOf(TeamNotFoundException.class) + .hasMessageContaining("id.not_found"); + } + + @Test + void addTeamToGroup_ShouldAddTeam_WhenGroupExists() { + when(groupRepository.findById(1L)).thenReturn(Optional.of(group)); + when(teamRepository.save(any(Team.class))).thenReturn(team); + + when(teamMapper.fromPayloadToEntity(teamPayload, group)).thenReturn(team); + when(teamMapper.fromEntityToResponse(team)).thenReturn(teamResponse); + + var result = teamService.addTeamToGroup(1L, teamPayload); + + assertThat(result).isEqualTo(teamResponse); + verify(teamRepository, times(1)).save(any(Team.class)); + } + + @Test + void addTeamToGroup_ShouldThrowUserNotFoundException_WhenUserDoesNotExist() { + when(groupRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> teamService.addTeamToGroup(1L, teamPayload)) + .isInstanceOf(GroupNotFoundException.class) + .hasMessageContaining("id.not_found"); + } + + @Test + void updateTeam_ShouldUpdateTeam_WhenTeamExists() { + when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); + when(teamRepository.save(any(Team.class))).thenReturn(team); + + when(teamMapper.updateEntityFromPayload(team, teamPayload)).thenReturn(team); + when(teamMapper.fromEntityToResponse(team)).thenReturn(teamResponse); + + var result = teamService.updateTeam(1L, teamPayload); + + assertAll( + () -> assertThat(result.description()).isEqualTo(teamPayload.description()), + () -> assertThat(result.name()).isEqualTo(teamPayload.name()) + ); + verify(teamRepository, times(1)).save(any(Team.class)); + } + + @Test + void updateTeam_ShouldThrowTeamNotFoundException_WhenTeamDoesNotExist() { + when(teamRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> teamService.updateTeam(1L, teamPayload)) + .isInstanceOf(TeamNotFoundException.class) + .hasMessageContaining("id.not_found"); + } + + @Test + void deleteTeam_ShouldDeleteTeam_WhenTeamExists() { + teamService.deleteTeam(1L); + + verify(teamRepository, times(1)).deleteById(1L); + } + + @Test + void deleteAllTeamsByGroup_ShouldDeleteAllTeams_WhenGroupExists() { + when(groupRepository.findById(1L)).thenReturn(Optional.of(group)); + when(teamRepository.findByGroup(group)).thenReturn(List.of(team)); + + teamService.deleteAllTeamsByGroup(1L); + + verify(teamRepository, times(1)).deleteAll(List.of(team)); + } + + @Test + void deleteAllTeamsByGroup_ShouldThrowGroupNotFoundException_WhenGroupDoesNotExist() { + when(groupRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> teamService.deleteAllTeamsByGroup(1L)) + .isInstanceOf(GroupNotFoundException.class) + .hasMessageContaining("id.not_found"); + } + +} \ No newline at end of file diff --git a/src/test/java/com/cf/cfteam/services/security/AuthenticationServiceTest.java b/src/test/java/com/cf/cfteam/services/security/AuthenticationServiceTest.java index 7106988..218898e 100644 --- a/src/test/java/com/cf/cfteam/services/security/AuthenticationServiceTest.java +++ b/src/test/java/com/cf/cfteam/services/security/AuthenticationServiceTest.java @@ -12,11 +12,11 @@ import com.cf.cfteam.transfer.payloads.security.ChangePasswordPayload; import com.cf.cfteam.transfer.payloads.security.RegistrationPayload; import com.cf.cfteam.transfer.responses.security.JwtAuthenticationResponse; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.PasswordEncoder; @@ -29,6 +29,7 @@ import static org.mockito.Mockito.*; @ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) class AuthenticationServiceTest { @InjectMocks @@ -55,11 +56,6 @@ class AuthenticationServiceTest { @Mock private UserDetails userDetails; - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - } - @Test void register_shouldThrowException_WhenUserAlreadyRegistered() { String login = "testLogin"; diff --git a/src/test/java/com/cf/cfteam/services/security/MyUserDetailsServiceTest.java b/src/test/java/com/cf/cfteam/services/security/MyUserDetailsServiceTest.java index 334b04c..834717b 100644 --- a/src/test/java/com/cf/cfteam/services/security/MyUserDetailsServiceTest.java +++ b/src/test/java/com/cf/cfteam/services/security/MyUserDetailsServiceTest.java @@ -2,11 +2,11 @@ import com.cf.cfteam.models.entities.security.User; import com.cf.cfteam.repositories.jpa.security.UserRepository; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.test.context.ActiveProfiles; @@ -18,6 +18,7 @@ import static org.mockito.Mockito.when; @ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) class MyUserDetailsServiceTest { @Mock @@ -26,11 +27,6 @@ class MyUserDetailsServiceTest { @InjectMocks private MyUserDetailsService userDetailsService; - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - } - @Test void loadUserByUsername_shouldLoadUserByUsername_WhenUserExists() { String login = "testLogin"; diff --git a/src/test/java/com/cf/cfteam/services/security/TokenServiceTest.java b/src/test/java/com/cf/cfteam/services/security/TokenServiceTest.java index bd46dd7..fb8b285 100644 --- a/src/test/java/com/cf/cfteam/services/security/TokenServiceTest.java +++ b/src/test/java/com/cf/cfteam/services/security/TokenServiceTest.java @@ -4,11 +4,11 @@ import com.cf.cfteam.exceptions.security.TokenNotFoundException; import com.cf.cfteam.models.entities.security.Token; import com.cf.cfteam.repositories.jpa.security.TokenRepository; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.context.ActiveProfiles; import java.util.Optional; @@ -18,6 +18,7 @@ import static org.mockito.Mockito.*; @ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) class TokenServiceTest { @InjectMocks @@ -26,11 +27,6 @@ class TokenServiceTest { @Mock private TokenRepository tokenRepository; - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - } - @Test void isTokenRevoked_shouldReturnTrue_whenTokenIsRevoked() { String tokenValue = "valid-token"; diff --git a/src/test/java/com/cf/cfteam/utils/codeforces/RatingCalculatorTest.java b/src/test/java/com/cf/cfteam/utils/codeforces/RatingCalculatorTest.java new file mode 100644 index 0000000..caea3d8 --- /dev/null +++ b/src/test/java/com/cf/cfteam/utils/codeforces/RatingCalculatorTest.java @@ -0,0 +1,26 @@ +package com.cf.cfteam.utils.codeforces; + +import com.cf.cfteam.transfer.responses.codeforces.PlayerResponse; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@ActiveProfiles("test") +class RatingCalculatorTest { + + @Test + void testCalculateSoloRating() { + PlayerResponse response = PlayerResponse.builder() + .id(1L) + .login("login") + .rating(1000.) + .build(); + List players = List.of(response); + var result = RatingCalculator.aggregateRatings(players); + + assertThat(result).isEqualTo(1000.); + } +} \ No newline at end of file diff --git a/src/test/java/com/cf/cfteam/utils/codeforces/mappers/GroupMapperTest.java b/src/test/java/com/cf/cfteam/utils/codeforces/mappers/GroupMapperTest.java new file mode 100644 index 0000000..89c99c0 --- /dev/null +++ b/src/test/java/com/cf/cfteam/utils/codeforces/mappers/GroupMapperTest.java @@ -0,0 +1,67 @@ +package com.cf.cfteam.utils.codeforces.mappers; + +import com.cf.cfteam.models.entities.codeforces.Group; +import com.cf.cfteam.models.entities.security.Role; +import com.cf.cfteam.models.entities.security.User; +import com.cf.cfteam.transfer.payloads.codeforces.GroupPayload; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) +class GroupMapperTest { + + @InjectMocks + private GroupMapper groupMapper; + + @Test + void shouldMapPayloadToEntity() { + GroupPayload payload = GroupPayload.builder() + .name("GroupName") + .description("GroupDescription") + .build(); + User user = User.builder() + .name("User name") + .login("User login") + .hashedPassword("Password") + .role(Role.USER) + .build(); + + + Group group = groupMapper.fromPayloadToEntity(payload, user); + + assertAll( + () -> assertThat(group).isNotNull(), + () -> assertThat(group.getName()).isEqualTo(payload.name()), + () -> assertThat(group.getDescription()).isEqualTo(payload.description()), + () -> assertThat(group.getUser()).isEqualTo(user) + ); + } + + @Test + void shouldUpdateEntityFromPayload() { + Group group = Group.builder() + .name("Test Group") + .description("Test description") + .build(); + + GroupPayload payload = GroupPayload.builder() + .name("NewName") + .description("NewDescription") + .build(); + + Group updatedGroup = groupMapper.updateEntityFromPayload(group, payload); + + assertAll( + () -> assertThat(updatedGroup).isNotNull(), + () -> assertThat(updatedGroup.getName()).isEqualTo(payload.name()), + () -> assertThat(updatedGroup.getDescription()).isEqualTo(payload.description()) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/cf/cfteam/utils/codeforces/mappers/PlayerMapperTest.java b/src/test/java/com/cf/cfteam/utils/codeforces/mappers/PlayerMapperTest.java new file mode 100644 index 0000000..d504991 --- /dev/null +++ b/src/test/java/com/cf/cfteam/utils/codeforces/mappers/PlayerMapperTest.java @@ -0,0 +1,60 @@ +package com.cf.cfteam.utils.codeforces.mappers; + +import com.cf.cfteam.models.entities.codeforces.Player; +import com.cf.cfteam.transfer.payloads.codeforces.PlayerPayload; +import com.cf.cfteam.transfer.responses.codeforces.PlayerResponse; +import com.github.benmanes.caffeine.cache.LoadingCache; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +@ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) +class PlayerMapperTest { + + @InjectMocks + private PlayerMapper playerMapper; + + @Mock + private LoadingCache ratingCache; + + @Test + void shouldMapFromEntityToResponse() { + Player player = Player.builder() + .login("login") + .id(1L) + .build(); + + when(ratingCache.get(player.getLogin())).thenReturn(0.); + + PlayerResponse response = playerMapper.fromEntityToResponse(player); + + assertAll( + () -> assertThat(response).isNotNull(), + () -> assertThat(response.id()).isEqualTo(player.getId()), + () -> assertThat(response.rating()).isEqualTo(0.), + () -> assertThat(response.login()).isEqualTo(player.getLogin()) + ); + } + + @Test + void shouldMapFromPayloadToEntity() { + PlayerPayload payload = PlayerPayload.builder() + .login("player login") + .build(); + + Player player = playerMapper.fromPayloadToEntity(payload); + + assertAll( + () -> assertThat(player).isNotNull(), + () -> assertThat(player.getLogin()).isEqualTo(payload.login()) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/cf/cfteam/utils/codeforces/mappers/TeamMapperTest.java b/src/test/java/com/cf/cfteam/utils/codeforces/mappers/TeamMapperTest.java new file mode 100644 index 0000000..4d5466c --- /dev/null +++ b/src/test/java/com/cf/cfteam/utils/codeforces/mappers/TeamMapperTest.java @@ -0,0 +1,89 @@ +package com.cf.cfteam.utils.codeforces.mappers; + +import com.cf.cfteam.models.entities.codeforces.Group; +import com.cf.cfteam.models.entities.codeforces.Team; +import com.cf.cfteam.transfer.payloads.codeforces.TeamPayload; +import com.cf.cfteam.transfer.responses.codeforces.TeamResponse; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) +class TeamMapperTest { + + @InjectMocks + private TeamMapper teamMapper; + + @Test + void shouldMapFromEntityToResponse() { + Team team = Team.builder() + .name("Team name") + .description(" Team description") + .id(1L) + .players(null) + .build(); + + TeamResponse response = teamMapper.fromEntityToResponse(team); + + assertAll( + () -> assertThat(response).isNotNull(), + () -> assertThat(response.name()).isEqualTo(team.getName()), + () -> assertThat(response.description()).isEqualTo(team.getDescription()), + () -> assertThat(response.id()).isEqualTo(team.getId()), + () -> assertThat(response.players()).isEqualTo(List.of()), + () -> assertThat(response.teamRating()).isEqualTo(0L) + ); + } + + @Test + void shouldMapFromPayloadToEntity() { + TeamPayload payload = TeamPayload.builder() + .name("team name") + .description("team description") + .build(); + Group group = Group.builder() + .name("group name") + .description("group descr") + .build(); + + Team team = teamMapper.fromPayloadToEntity(payload, group); + + assertAll( + () -> assertThat(team).isNotNull(), + () -> assertThat(team.getGroup()).isEqualTo(group), + () -> assertThat(team.getName()).isEqualTo(payload.name()), + () -> assertThat(team.getDescription()).isEqualTo(payload.description()) + ); + } + + @Test + void shouldUpdateEntityFromPayload() { + Team team = Team.builder() + .name("Team name") + .description(" Team description") + .id(1L) + .players(null) + .build(); + + TeamPayload payload = TeamPayload.builder() + .name("new team name") + .description("new team description") + .build(); + + Team updatedTeam = teamMapper.updateEntityFromPayload(team, payload); + + assertAll( + () -> assertThat(updatedTeam).isNotNull(), + () -> assertThat(updatedTeam.getName()).isEqualTo(payload.name()), + () -> assertThat(updatedTeam.getDescription()).isEqualTo(payload.description()) + ); + } +} \ No newline at end of file diff --git a/src/test/resources/codeforces/responses/tourist.json b/src/test/resources/codeforces/responses/tourist.json new file mode 100644 index 0000000..0cd2b57 --- /dev/null +++ b/src/test/resources/codeforces/responses/tourist.json @@ -0,0 +1,9 @@ +{ + "status": "OK", + "result": [ + { + "handle": "tourist", + "rating": 4009 + } + ] +}