From 756ce3b0b5b1f13fcba66081e4a6dec426d37422 Mon Sep 17 00:00:00 2001 From: toximu Date: Wed, 20 May 2026 13:13:46 +0300 Subject: [PATCH 1/5] picks-bans system + tests --- build.gradle | 6 +- .../AuthController/AuthController.java | 15 +- .../JWTRequestFilter/JWTRequestFilter.java | 23 +- .../Authentication/JWTUtils/JWTUtils.java | 55 +- .../Authentication/config/SecurityConfig.java | 2 +- .../com/codzilla/backend/ExampleEndpoint.java | 2 +- .../PreMatch/DraftSession/DraftSession.java | 59 +++ .../DraftSession/DraftSessionRepository.java | 18 + .../DraftSession/DraftSessionService.java | 196 ++++++++ .../DraftSession/DraftSessionSettings.java | 16 + .../PicksBans/JsonToMapOptionsConverter.java | 33 ++ .../PreMatch/PicksBans/PicksBansConfig.java | 25 + .../PicksBans/PicksBansController.java | 84 ++++ .../backend/PreMatch/model/Category.java | 17 + .../PreMatch/model/DraftSessionException.java | 7 + .../model/DraftSessionResponseDTO.java | 21 + .../backend/PreMatch/model/Language.java | 6 + .../backend/PreMatch/model/OptionEntity.java | 16 + .../backend/PreMatch/model/ProblemLevel.java | 7 + .../backend/PreMatch/model/ProblemType.java | 7 + .../java/com/codzilla/backend/User/User.java | 2 +- .../backend/WebSocket/WebSocketConfig.java | 48 ++ src/main/resources/application.properties | 5 +- .../Authentication/BaseIntegrationTest.java | 24 - .../Authentication/JwtIntegrationTest.java | 1 + .../Authentication/LoginIntegrationTest.java | 1 + .../RegisterIntegrationTest.java | 1 + .../RoleAccessIntegrationTest.java | 1 + .../backend/BackendApplicationTests.java | 2 +- .../codzilla/backend/BaseIntegrationTest.java | 59 +++ .../DraftSession/DraftSessionServiceTest.java | 474 ++++++++++++++++++ .../PicksBans/PicksBansControllerTest.java | 137 +++++ .../codzilla/backend/S3/S3RepositoryTest.java | 3 +- .../integration/ProblemIntegrationTest.java | 36 +- src/test/resources/application.properties | 14 +- 35 files changed, 1341 insertions(+), 82 deletions(-) create mode 100644 src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSession.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionRepository.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionService.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionSettings.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/PicksBans/JsonToMapOptionsConverter.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansConfig.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansController.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/model/Category.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/model/DraftSessionException.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/model/DraftSessionResponseDTO.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/model/Language.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/model/OptionEntity.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/model/ProblemLevel.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/model/ProblemType.java create mode 100644 src/main/java/com/codzilla/backend/WebSocket/WebSocketConfig.java delete mode 100644 src/test/java/com/codzilla/backend/Authentication/BaseIntegrationTest.java create mode 100644 src/test/java/com/codzilla/backend/BaseIntegrationTest.java create mode 100644 src/test/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionServiceTest.java create mode 100644 src/test/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansControllerTest.java diff --git a/build.gradle b/build.gradle index 0b39757..ffeb502 100644 --- a/build.gradle +++ b/build.gradle @@ -53,9 +53,11 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-security-test' testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.testcontainers:testcontainers-postgresql' + testImplementation 'org.springframework.boot:spring-boot-testcontainers' + testImplementation "org.testcontainers:testcontainers-minio" + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - testRuntimeOnly 'com.h2database:h2' - testImplementation 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/src/main/java/com/codzilla/backend/Authentication/AuthController/AuthController.java b/src/main/java/com/codzilla/backend/Authentication/AuthController/AuthController.java index 772438a..0bed216 100644 --- a/src/main/java/com/codzilla/backend/Authentication/AuthController/AuthController.java +++ b/src/main/java/com/codzilla/backend/Authentication/AuthController/AuthController.java @@ -46,7 +46,9 @@ public ResponseEntity login(@RequestBody LoginRequestDTO request, HttpServlet new UsernamePasswordAuthenticationToken(request.email(), request.rawPassword()) ); - var accessToken = jwtUtils.generateAccessToken(auth); + User user = (User) auth.getPrincipal(); + + var accessToken = jwtUtils.generateAccessToken(user); Cookie jwtCookie = new Cookie("jwt", accessToken); jwtCookie.setHttpOnly(true); jwtCookie.setSecure(false); @@ -54,15 +56,13 @@ public ResponseEntity login(@RequestBody LoginRequestDTO request, HttpServlet jwtCookie.setMaxAge((int) settings.getRefreshTokenTtl().toSeconds()); response.addCookie(jwtCookie); - var refreshToken = jwtUtils.generateRefreshToken(auth); + var refreshToken = jwtUtils.generateRefreshToken(user); Cookie refreshCookie = new Cookie("refresh_jwt", refreshToken); refreshCookie.setPath("/"); refreshCookie.setHttpOnly(true); refreshCookie.setMaxAge((int) settings.getRefreshTokenTtl().toSeconds()); refreshCookie.setSecure(false); response.addCookie(refreshCookie); - - User user = userService.getByEmail(request.email()); return ResponseEntity.ok(new LoginResponseDTO(user.getNickname())); } @@ -104,13 +104,8 @@ public ResponseEntity refreshToken(HttpServletRequest request, HttpServletRes User user = userService.getByEmail(email); - UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken( - user, - null, - user.getAuthorities() - ); - var accessToken = jwtUtils.generateAccessToken(auth); + var accessToken = jwtUtils.generateAccessToken(user); Cookie cookie = new Cookie("jwt", accessToken); cookie.setHttpOnly(true); cookie.setSecure(false); diff --git a/src/main/java/com/codzilla/backend/Authentication/JWTRequestFilter/JWTRequestFilter.java b/src/main/java/com/codzilla/backend/Authentication/JWTRequestFilter/JWTRequestFilter.java index 45ad909..cbd6eff 100644 --- a/src/main/java/com/codzilla/backend/Authentication/JWTRequestFilter/JWTRequestFilter.java +++ b/src/main/java/com/codzilla/backend/Authentication/JWTRequestFilter/JWTRequestFilter.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.util.List; +import java.util.UUID; import com.codzilla.backend.Authentication.JWTUtils.JWTUtils; @@ -27,10 +28,12 @@ public JWTRequestFilter(JWTUtils jwtUtils) { } @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { log.info("In filter."); String token = null; if (request.getCookies() != null) { + log.info("COOKIE: " + request.getCookies().toString()); for (var cookie : request.getCookies()) { log.info("Cookie: " + cookie.getName()); if ("jwt".equals(cookie.getName())) { @@ -45,14 +48,19 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse try { String email = jwtUtils.getEmailFromToken(token); List roles = jwtUtils.getRolesFromToken(token); - log.info("{} has jwt. His roles: {}", email, roles); + UUID uuid = jwtUtils.getIdFromToken(token); + log.info( + "{} has jwt. His roles: {}", + email, + roles + ); List authorities = roles.stream() - .map(SimpleGrantedAuthority::new) - .toList(); + .map(SimpleGrantedAuthority::new) + .toList(); User user = User.builder() - .email(email).build(); + .email(email).id(uuid).build(); UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken( user, null, @@ -69,7 +77,10 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } - filterChain.doFilter(request, response); + filterChain.doFilter( + request, + response + ); } } diff --git a/src/main/java/com/codzilla/backend/Authentication/JWTUtils/JWTUtils.java b/src/main/java/com/codzilla/backend/Authentication/JWTUtils/JWTUtils.java index da920db..f903264 100644 --- a/src/main/java/com/codzilla/backend/Authentication/JWTUtils/JWTUtils.java +++ b/src/main/java/com/codzilla/backend/Authentication/JWTUtils/JWTUtils.java @@ -1,6 +1,7 @@ package com.codzilla.backend.Authentication.JWTUtils; import com.codzilla.backend.Authentication.config.AuthSettings; +import com.codzilla.backend.User.User; import io.jsonwebtoken.*; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; @@ -9,6 +10,7 @@ import javax.crypto.SecretKey; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.UUID; @Component @@ -21,27 +23,39 @@ public JWTUtils(AuthSettings settings) { this.settings = settings; } - public String generateAccessToken(Authentication authentication) { - List roles = authentication.getAuthorities().stream() - .map(GrantedAuthority::getAuthority) - .filter(role -> !role.equals("FACTOR_PASSWORD")) - .toList(); + public String generateAccessToken(User user) { + List roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .filter(role -> !Objects.equals( + role, + "FACTOR_PASSWORD" + )) + .toList(); return Jwts.builder() - .subject(authentication.getName()) - .claim("roles", roles) + .subject(user.getEmail()) + .claim( + "roles", + roles + ) + .claim( + "id", + user.getId() + ) .issuedAt(new Date()) - .expiration(new Date(System.currentTimeMillis() + settings.getAccessTokenTtl().toMillis())) + .expiration(new Date( + System.currentTimeMillis() + settings.getAccessTokenTtl().toMillis())) .signWith(secret) .compact(); } - public String generateRefreshToken(Authentication authentication) { + public String generateRefreshToken(User user) { return Jwts.builder() - .subject(authentication.getName()) + .subject(user.getEmail()) .setId(UUID.randomUUID().toString()) .issuedAt(new Date()) - .expiration(new Date(System.currentTimeMillis() + settings.getRefreshTokenTtl().toMillis())) + .expiration(new Date( + System.currentTimeMillis() + settings.getRefreshTokenTtl().toMillis())) .signWith(secret) .compact(); } @@ -53,7 +67,10 @@ public List getRolesFromToken(String token) { .parseSignedClaims(token) .getPayload(); - return claims.get("roles", List.class); + return claims.get( + "roles", + List.class + ); } public String getEmailFromToken(String token) { @@ -65,6 +82,20 @@ public String getEmailFromToken(String token) { .getSubject(); } + public UUID getIdFromToken(String token) { + Claims claims = Jwts.parser() + .verifyWith(secret) + .build() + .parseSignedClaims(token) + .getPayload(); + + String stringUUID = claims.get( + "id", + String.class + ); + return UUID.fromString(stringUUID); + } + public boolean validateToken(String token) { try { Jwts.parser() diff --git a/src/main/java/com/codzilla/backend/Authentication/config/SecurityConfig.java b/src/main/java/com/codzilla/backend/Authentication/config/SecurityConfig.java index 4bf1654..19cba04 100644 --- a/src/main/java/com/codzilla/backend/Authentication/config/SecurityConfig.java +++ b/src/main/java/com/codzilla/backend/Authentication/config/SecurityConfig.java @@ -38,7 +38,7 @@ public SecurityFilterChain filterChain(HttpSecurity http, JWTRequestFilter filte return http .cors(cors -> cors.configurationSource(request -> { CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOrigins(Arrays.asList("http://localhost:5173")); + config.setAllowedOriginPatterns(Arrays.asList("*")); config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowedHeaders(Arrays.asList("*")); config.setAllowCredentials(true); diff --git a/src/main/java/com/codzilla/backend/ExampleEndpoint.java b/src/main/java/com/codzilla/backend/ExampleEndpoint.java index bee7df9..22d20c9 100644 --- a/src/main/java/com/codzilla/backend/ExampleEndpoint.java +++ b/src/main/java/com/codzilla/backend/ExampleEndpoint.java @@ -12,4 +12,4 @@ public class ExampleEndpoint { public ResponseEntity> endpoint() { return ResponseEntity.ok(Map.of("message", "info")); } -} +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSession.java b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSession.java new file mode 100644 index 0000000..f5300b5 --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSession.java @@ -0,0 +1,59 @@ +package com.codzilla.backend.PreMatch.DraftSession; + + +import com.codzilla.backend.PreMatch.model.*; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; +import java.util.*; + +@Entity +@Table(name = "lobby") +@Getter +@Setter +public class DraftSession { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @JdbcTypeCode(SqlTypes.UUID) + UUID id; + + @JdbcTypeCode(SqlTypes.UUID) + private UUID firstUserId; + + @JdbcTypeCode(SqlTypes.UUID) + private UUID secondUserId; + + private Status status = Status.PICKING; + + boolean isFirstUserMove; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "remain_options", columnDefinition = "jsonb") + Map> remainOptions = new HashMap<>(); + + public DraftSession() { + isFirstUserMove = true; + for (var category : Category.values()) { + remainOptions.put(category, new HashSet<>()); + for (var option : category.getEnumClass().getEnumConstants()) { + remainOptions.get(category).add(option.name()); + } + } + } + + + public DraftSession(UUID firstUserId, UUID secondUserId) { + this(); + this.firstUserId = firstUserId; + this.secondUserId = secondUserId; + } + + public enum Status { + PICKING, + FINISHED + } +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionRepository.java b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionRepository.java new file mode 100644 index 0000000..0a08c7c --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionRepository.java @@ -0,0 +1,18 @@ +package com.codzilla.backend.PreMatch.DraftSession; + +import jakarta.persistence.LockModeType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; +import java.util.UUID; + +public interface DraftSessionRepository extends JpaRepository { + + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select d from DraftSession d where d.id = :id") + Optional findByIdWithLock(@Param("id") UUID id); +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionService.java b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionService.java new file mode 100644 index 0000000..754524c --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionService.java @@ -0,0 +1,196 @@ +package com.codzilla.backend.PreMatch.DraftSession; + +import com.codzilla.backend.PreMatch.model.*; +import com.codzilla.backend.User.User; +import jakarta.transaction.Transactional; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Service; +import org.springframework.transaction.support.TransactionTemplate; + +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ThreadLocalRandom; + +@Slf4j +@Service +public class DraftSessionService { + + public DraftSessionService(DraftSessionRepository draftSessionRepository, + @Qualifier("threadPoolTaskScheduler") TaskScheduler taskScheduler, + SimpMessagingTemplate messagingTemplate, + TransactionTemplate transactionTemplate, + DraftSessionSettings settings + ) { + this.draftSessionRepository = draftSessionRepository; + this.taskScheduler = taskScheduler; + this.messagingTemplate = messagingTemplate; + this.transactionTemplate = transactionTemplate; + this.settings = settings; + } + + + private final DraftSessionRepository draftSessionRepository; + private final TaskScheduler taskScheduler; + private final SimpMessagingTemplate messagingTemplate; + private final TransactionTemplate transactionTemplate; + private final DraftSessionSettings settings; + + private final Map> scheduledTasks = new ConcurrentHashMap<>(); + + @Transactional + public DraftSession processBan(User user, UUID draftSessionId, OptionEntity optionEntity) { + var draftSession = draftSessionRepository.findByIdWithLock(draftSessionId).orElseThrow( + () -> new RuntimeException( + "There is no lobby: " + draftSessionId)); + + cancelTimer(draftSessionId); + + var nowUserMoving = + draftSession.isFirstUserMove ? draftSession.getFirstUserId() : + draftSession.getSecondUserId(); + + if (!nowUserMoving.equals(user.getId())) { + throw new DraftSessionException("It is not your turn now!"); + } + + banOption( + draftSession, + optionEntity + ); + + if (draftSession.getStatus().equals(DraftSession.Status.PICKING)) { + startTimer(draftSessionId); + } + + draftSessionRepository.save(draftSession); + + return draftSession; + } + + private void banOption(DraftSession draftSession, OptionEntity optionEntity) { + + + boolean isOptionExists = + Arrays.stream(optionEntity.getCategory().getEnumClass().getEnumConstants()) + .anyMatch( + anEnum -> anEnum.name().equals(optionEntity.getBanObject()) + ); + + + if (!isOptionExists) { + throw new DraftSessionException("Invalid ban option!"); + } + + long optionsOfCategoryRemain = + draftSession.remainOptions.get(optionEntity.getCategory()).size(); + + if (optionsOfCategoryRemain == 1) { + throw new DraftSessionException("There is only one object in this category! You cant " + + "ban it."); + } + + if (!draftSession.remainOptions.get(optionEntity.getCategory()) + .contains(optionEntity.getBanObject())) { + throw new DraftSessionException("Already banned: " + optionEntity.getBanObject()); + } + + draftSession.remainOptions.get(optionEntity.getCategory()) + .remove(optionEntity.getBanObject()); + draftSession.setFirstUserMove(!draftSession.isFirstUserMove()); + + boolean isDraftSessionFinished = + draftSession.remainOptions.values().stream().allMatch( + set -> set.size() == 1 + ); + + if (isDraftSessionFinished) { + draftSession.setStatus(DraftSession.Status.FINISHED); + } + } + + private void startTimer(UUID draftSessionId) { + + ScheduledFuture task = taskScheduler.schedule( + () -> { + try { + transactionTemplate.executeWithoutResult(status -> { + executeRandomBan(draftSessionId); + }); + } catch (Exception e) { + log.error( + "Ошибка при автоматическом бане для сессии {}", + draftSessionId, + e + ); + } + }, + Instant.now().plus(settings.getTimeToPick()) + ); + scheduledTasks.put( + draftSessionId, + task + ); + } + + private void cancelTimer(UUID draftSessionId) { + ScheduledFuture task = scheduledTasks.remove(draftSessionId); + if (task != null) { + task.cancel(false); + } + } + + @Transactional + public void executeRandomBan(UUID draftSessionId) { + DraftSession draftSession = + draftSessionRepository.findByIdWithLock(draftSessionId).orElseThrow(); + + if (scheduledTasks.get(draftSessionId) == null) { + return; + } + + List allOptions = draftSession.remainOptions.entrySet().stream() + .flatMap(entry -> entry.getValue() + .stream() + .map(value -> new OptionEntity( + entry.getKey(), + value + ))) + .toList(); + + while (true) { + int randomIndex = ThreadLocalRandom.current().nextInt(allOptions.size()); + try { + banOption( + draftSession, + allOptions.get(randomIndex) + ); + log.info("BANNED"); + break; + } catch (RuntimeException _) { + } + } + + + draftSessionRepository.save(draftSession); + + messagingTemplate.convertAndSend( + "/topic/draft-session/" + draftSessionId, + new DraftSessionResponseDTO( + DraftSessionResponseDTO.Status.SUCCEED, + draftSession, + null + ) + ); + if (draftSession.getStatus().equals(DraftSession.Status.PICKING)) { + startTimer(draftSessionId); + } + } +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionSettings.java b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionSettings.java new file mode 100644 index 0000000..8a2cac7 --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionSettings.java @@ -0,0 +1,16 @@ +package com.codzilla.backend.PreMatch.DraftSession; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; + +@Configuration +@ConfigurationProperties(prefix = "app.draft-session") +@Getter +@Setter +public class DraftSessionSettings { + Duration timeToPick; +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/PicksBans/JsonToMapOptionsConverter.java b/src/main/java/com/codzilla/backend/PreMatch/PicksBans/JsonToMapOptionsConverter.java new file mode 100644 index 0000000..4becc49 --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/PicksBans/JsonToMapOptionsConverter.java @@ -0,0 +1,33 @@ +package com.codzilla.backend.infrastructure; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import com.codzilla.backend.PreMatch.model.Category; +import java.util.*; + +@Converter +public class JsonToMapOptionsConverter implements AttributeConverter>, String> { + + private final static ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public String convertToDatabaseColumn(Map> attribute) { + try { + return objectMapper.writeValueAsString(attribute); + } catch (JsonProcessingException e) { + return null; + } + } + + @Override + public Map> convertToEntityAttribute(String dbData) { + try { + return objectMapper.readValue(dbData, new TypeReference>>() {}); + } catch (JsonProcessingException e) { + return new HashMap<>(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansConfig.java b/src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansConfig.java new file mode 100644 index 0000000..aac2bc6 --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansConfig.java @@ -0,0 +1,25 @@ +package com.codzilla.backend.PreMatch.PicksBans; + + +import com.codzilla.backend.PreMatch.model.Category; +import com.codzilla.backend.PreMatch.model.Language; +import com.codzilla.backend.PreMatch.model.ProblemLevel; +import com.codzilla.backend.PreMatch.model.ProblemType; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +@Configuration +public class PicksBansConfig { + + @Bean + public ThreadPoolTaskScheduler threadPoolTaskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + return scheduler; + } +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansController.java b/src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansController.java new file mode 100644 index 0000000..fa56adf --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansController.java @@ -0,0 +1,84 @@ +package com.codzilla.backend.PreMatch.PicksBans; + +import com.codzilla.backend.PreMatch.DraftSession.DraftSession; +import com.codzilla.backend.PreMatch.DraftSession.DraftSessionRepository; +import com.codzilla.backend.PreMatch.DraftSession.DraftSessionService; +import com.codzilla.backend.PreMatch.model.DraftSessionResponseDTO; +import com.codzilla.backend.PreMatch.model.OptionEntity; +import com.codzilla.backend.PreMatch.model.DraftSessionException; +import com.codzilla.backend.User.User; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.stereotype.Controller; + +import java.security.Principal; +import java.util.UUID; + +@Slf4j +@Controller +public class PicksBansController { + private final DraftSessionRepository draftSessionRepository; + + public PicksBansController(SimpMessagingTemplate messagingTemplate, + DraftSessionService draftSessionService, + DraftSessionRepository draftSessionRepository) { + this.draftSessionService = draftSessionService; + this.messagingTemplate = messagingTemplate; + this.draftSessionRepository = draftSessionRepository; + } + + DraftSessionService draftSessionService; + SimpMessagingTemplate messagingTemplate; + + @MessageMapping("{draftSessionId}/ban") + void handleBanRequest( + @DestinationVariable String draftSessionId, + @AuthenticationPrincipal Authentication auth, + @Payload OptionEntity optionEntity) { + User user = (User) auth.getPrincipal(); + log.info("In ban controller"); + log.info( + "User : {}", + user + ); + log.info( + "Request: {}", + optionEntity.getBanObject() + ); + log.info("All sessions: {}", draftSessionRepository.findAll().stream().map(DraftSession::getId).toList()); + try { + assert user != null; + var draftSession = draftSessionService.processBan( + user, + UUID.fromString(draftSessionId), + optionEntity + ); + messagingTemplate.convertAndSend( + "/topic/draft-session/" + draftSessionId, + new DraftSessionResponseDTO( + DraftSessionResponseDTO.Status.SUCCEED, + draftSession, + null + ) + ); + } catch (DraftSessionException exception) { + log.info( + "Exception: {}", + exception.toString() + ); + messagingTemplate.convertAndSend( + "/topic/draft-session/" + draftSessionId, + new DraftSessionResponseDTO( + DraftSessionResponseDTO.Status.ERROR, + null, + exception.getMessage() + ) + ); + } + } +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/model/Category.java b/src/main/java/com/codzilla/backend/PreMatch/model/Category.java new file mode 100644 index 0000000..55dd1d1 --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/model/Category.java @@ -0,0 +1,17 @@ +package com.codzilla.backend.PreMatch.model; + +public enum Category { + Language(Language.class), + ProblemType(ProblemType.class), + ProblemLevel(ProblemLevel.class); + + private final Class> enumClass; + + Category(Class> enumClass) { + this.enumClass = enumClass; + } + + public Class> getEnumClass() { + return enumClass; + } +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/PreMatch/model/DraftSessionException.java b/src/main/java/com/codzilla/backend/PreMatch/model/DraftSessionException.java new file mode 100644 index 0000000..6115ec0 --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/model/DraftSessionException.java @@ -0,0 +1,7 @@ +package com.codzilla.backend.PreMatch.model; + +public class DraftSessionException extends RuntimeException { + public DraftSessionException(String message) { + super(message); + } +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/model/DraftSessionResponseDTO.java b/src/main/java/com/codzilla/backend/PreMatch/model/DraftSessionResponseDTO.java new file mode 100644 index 0000000..1f29732 --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/model/DraftSessionResponseDTO.java @@ -0,0 +1,21 @@ +package com.codzilla.backend.PreMatch.model; + +import com.codzilla.backend.PreMatch.DraftSession.DraftSession; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + + +@AllArgsConstructor +@Getter +@Setter +public class DraftSessionResponseDTO { + Status status; + DraftSession draftSession; + String error; + + public enum Status { + ERROR, + SUCCEED + } +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/model/Language.java b/src/main/java/com/codzilla/backend/PreMatch/model/Language.java new file mode 100644 index 0000000..4f3491e --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/model/Language.java @@ -0,0 +1,6 @@ +package com.codzilla.backend.PreMatch.model; + +public enum Language { + CPP, + PY +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/model/OptionEntity.java b/src/main/java/com/codzilla/backend/PreMatch/model/OptionEntity.java new file mode 100644 index 0000000..4d9d0c5 --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/model/OptionEntity.java @@ -0,0 +1,16 @@ +package com.codzilla.backend.PreMatch.model; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class OptionEntity { + Category category; + String banObject; + + public OptionEntity(Category category, String banObject) { + this.category = category; + this.banObject = banObject; + } +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/model/ProblemLevel.java b/src/main/java/com/codzilla/backend/PreMatch/model/ProblemLevel.java new file mode 100644 index 0000000..7830020 --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/model/ProblemLevel.java @@ -0,0 +1,7 @@ +package com.codzilla.backend.PreMatch.model; + +public enum ProblemLevel { + Easy, + Medium, + Hard +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/model/ProblemType.java b/src/main/java/com/codzilla/backend/PreMatch/model/ProblemType.java new file mode 100644 index 0000000..041f61d --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/model/ProblemType.java @@ -0,0 +1,7 @@ +package com.codzilla.backend.PreMatch.model; + +public enum ProblemType { + Algorithm, + Shell, + SQL +} diff --git a/src/main/java/com/codzilla/backend/User/User.java b/src/main/java/com/codzilla/backend/User/User.java index 2eb5921..9ba9186 100644 --- a/src/main/java/com/codzilla/backend/User/User.java +++ b/src/main/java/com/codzilla/backend/User/User.java @@ -35,7 +35,7 @@ public class User implements UserDetails, CredentialsContainer { private String email; - private int rating = 100; + private Integer rating = 100; @Id @GeneratedValue(strategy = GenerationType.UUID) @JdbcTypeCode(SqlTypes.UUID) diff --git a/src/main/java/com/codzilla/backend/WebSocket/WebSocketConfig.java b/src/main/java/com/codzilla/backend/WebSocket/WebSocketConfig.java new file mode 100644 index 0000000..2ced0d8 --- /dev/null +++ b/src/main/java/com/codzilla/backend/WebSocket/WebSocketConfig.java @@ -0,0 +1,48 @@ +package com.codzilla.backend.WebSocket; + + +import com.codzilla.backend.User.User; +import com.sun.security.auth.UserPrincipal; +import lombok.extern.slf4j.Slf4j; +import org.jspecify.annotations.Nullable; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import org.springframework.web.socket.server.HandshakeInterceptor; +import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor; + +import java.util.Map; + +@Slf4j +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + config.enableSimpleBroker("/topic"); + config.setApplicationDestinationPrefixes("/app"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + .addEndpoint("/game-ws") + .setAllowedOriginPatterns("*").withSockJS(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 680fb53..aed14ef 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,4 @@ spring.application.name=Backend -spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=update app.security.jwt.access-token-ttl=10m @@ -11,6 +10,8 @@ app.s3.secret-key=${S3_PASSWORD} app.s3.region=${S3_REGION} app.s3.bucket-name=${S3_BUCKET_NAME} +app.draft-session.time-to-pick=10s + spring.datasource.url=${DB_URL} spring.datasource.username=${DB_USERNAME} spring.datasource.password=${DB_PASSWORD} @@ -28,3 +29,5 @@ polygon.api.secret=${POLYGON_SECRET} polygon.api.url=${POLYGON_API_URL:https://polygon.codeforces.com/api/} judge0.base-url=${JUDGE0_BASE_URL} +logging.level.org.springframework.messaging=TRACE +logging.level.org.springframework.web.socket=TRACE \ No newline at end of file diff --git a/src/test/java/com/codzilla/backend/Authentication/BaseIntegrationTest.java b/src/test/java/com/codzilla/backend/Authentication/BaseIntegrationTest.java deleted file mode 100644 index 1b603e1..0000000 --- a/src/test/java/com/codzilla/backend/Authentication/BaseIntegrationTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.codzilla.backend.Authentication; - -import com.codzilla.backend.User.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.transaction.annotation.Transactional; -import tools.jackson.databind.ObjectMapper; - - -@SpringBootTest -@AutoConfigureMockMvc -@Transactional -public class BaseIntegrationTest { - @Autowired - protected MockMvc mockMvc; - - @Autowired - protected ObjectMapper objectMapper; - - @Autowired - protected UserRepository userRepository; -} diff --git a/src/test/java/com/codzilla/backend/Authentication/JwtIntegrationTest.java b/src/test/java/com/codzilla/backend/Authentication/JwtIntegrationTest.java index 50c228a..3bfb543 100644 --- a/src/test/java/com/codzilla/backend/Authentication/JwtIntegrationTest.java +++ b/src/test/java/com/codzilla/backend/Authentication/JwtIntegrationTest.java @@ -5,6 +5,7 @@ import com.codzilla.backend.Authentication.config.AuthSettings; import com.codzilla.backend.Authentication.dto.LoginRequestDTO; import com.codzilla.backend.Authentication.dto.RegisterRequestDTO; +import com.codzilla.backend.BaseIntegrationTest; import jakarta.transaction.Transactional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/codzilla/backend/Authentication/LoginIntegrationTest.java b/src/test/java/com/codzilla/backend/Authentication/LoginIntegrationTest.java index 2da0434..7c74dec 100644 --- a/src/test/java/com/codzilla/backend/Authentication/LoginIntegrationTest.java +++ b/src/test/java/com/codzilla/backend/Authentication/LoginIntegrationTest.java @@ -4,6 +4,7 @@ import com.codzilla.backend.Authentication.dto.LoginRequestDTO; import com.codzilla.backend.Authentication.dto.LoginResponseDTO; import com.codzilla.backend.Authentication.dto.RegisterRequestDTO; +import com.codzilla.backend.BaseIntegrationTest; import jakarta.transaction.Transactional; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/test/java/com/codzilla/backend/Authentication/RegisterIntegrationTest.java b/src/test/java/com/codzilla/backend/Authentication/RegisterIntegrationTest.java index 3bb67da..772c6c5 100644 --- a/src/test/java/com/codzilla/backend/Authentication/RegisterIntegrationTest.java +++ b/src/test/java/com/codzilla/backend/Authentication/RegisterIntegrationTest.java @@ -1,5 +1,6 @@ package com.codzilla.backend.Authentication; +import com.codzilla.backend.BaseIntegrationTest; import com.codzilla.backend.User.User; import com.codzilla.backend.Authentication.Exceptions.UserAlreadyExistsException; import com.codzilla.backend.Authentication.dto.ErrorResponseDTO; diff --git a/src/test/java/com/codzilla/backend/Authentication/RoleAccessIntegrationTest.java b/src/test/java/com/codzilla/backend/Authentication/RoleAccessIntegrationTest.java index 462a722..2a5c63c 100644 --- a/src/test/java/com/codzilla/backend/Authentication/RoleAccessIntegrationTest.java +++ b/src/test/java/com/codzilla/backend/Authentication/RoleAccessIntegrationTest.java @@ -3,6 +3,7 @@ import com.codzilla.backend.Authentication.dto.LoginRequestDTO; import com.codzilla.backend.Authentication.dto.RegisterRequestDTO; +import com.codzilla.backend.BaseIntegrationTest; import jakarta.transaction.Transactional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/codzilla/backend/BackendApplicationTests.java b/src/test/java/com/codzilla/backend/BackendApplicationTests.java index f44ee9f..529417a 100644 --- a/src/test/java/com/codzilla/backend/BackendApplicationTests.java +++ b/src/test/java/com/codzilla/backend/BackendApplicationTests.java @@ -4,7 +4,7 @@ import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class BackendApplicationTests { +class BackendApplicationTests extends BaseIntegrationTest { @Test void contextLoads() { diff --git a/src/test/java/com/codzilla/backend/BaseIntegrationTest.java b/src/test/java/com/codzilla/backend/BaseIntegrationTest.java new file mode 100644 index 0000000..4aa9696 --- /dev/null +++ b/src/test/java/com/codzilla/backend/BaseIntegrationTest.java @@ -0,0 +1,59 @@ +package com.codzilla.backend; + +import com.codzilla.backend.User.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.containers.MinIOContainer; +import org.testcontainers.containers.PostgreSQLContainer; +import tools.jackson.databind.ObjectMapper; + + +@SpringBootTest +@AutoConfigureMockMvc +public class BaseIntegrationTest { + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected ObjectMapper objectMapper; + + @Autowired + protected UserRepository userRepository; + + + @ServiceConnection + static PostgreSQLContainer postgres + = new PostgreSQLContainer<>("postgres:16-alpine"); + + + static MinIOContainer minio = new MinIOContainer("minio/minio:latest") + .withUserName("test-user") + .withPassword("test-password"); + + @DynamicPropertySource + static void overrideProps(DynamicPropertyRegistry registry) { + registry.add( + "app.s3.endpoint", + minio::getS3URL + ); + registry.add( + "app.s3.access-key", + minio::getUserName + ); + registry.add( + "app.s3.secret-key", + minio::getPassword + ); + } + + static { + postgres.start(); + minio.start(); + } +} diff --git a/src/test/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionServiceTest.java b/src/test/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionServiceTest.java new file mode 100644 index 0000000..20936ae --- /dev/null +++ b/src/test/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionServiceTest.java @@ -0,0 +1,474 @@ +package com.codzilla.backend.PreMatch.DraftSession; + +import com.codzilla.backend.Authentication.dto.RegisterRequestDTO; +import com.codzilla.backend.BaseIntegrationTest; +import com.codzilla.backend.PreMatch.model.*; +import com.codzilla.backend.User.User; +import com.codzilla.backend.User.UserRepository; +import com.codzilla.backend.User.UserService; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.MediaType; +import org.springframework.messaging.converter.JacksonJsonMessageConverter; +import org.springframework.messaging.simp.stomp.StompHeaders; +import org.springframework.messaging.simp.stomp.StompSession; +import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.web.servlet.support.WebContentGenerator; +import org.springframework.web.socket.WebSocketHttpHeaders; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; +import org.springframework.web.socket.messaging.WebSocketStompClient; +import org.springframework.web.socket.sockjs.client.SockJsClient; +import org.springframework.web.socket.sockjs.client.Transport; +import org.springframework.web.socket.sockjs.client.WebSocketTransport; +import org.testcontainers.shaded.org.checkerframework.checker.units.qual.A; + +import java.lang.reflect.Type; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class DraftSessionServiceTest extends BaseIntegrationTest { + + private static final Logger log = LoggerFactory.getLogger(DraftSessionServiceTest.class); + @LocalServerPort + private int port; + + @MockitoBean + DraftSessionSettings draftSessionSettings; + + private RestTestClient restTestClient; + + private BlockingQueue user1Queue; + private BlockingQueue user2Queue; + + private StompSession user1Session; + private StompSession user2Session; + + @Autowired + private UserRepository userRepository; + + @Autowired + private DraftSessionRepository draftSessionRepository; + + @Autowired + private UserService userService; + + private User user1; + private User user2; + + String user1Jwt; + String user2Jwt; + + private DraftSession draftSession; + @Autowired + private WebContentGenerator webContentGenerator; + + @BeforeEach + void setup() throws Exception { + restTestClient = RestTestClient.bindToServer().baseUrl("http://localhost:" + port).build(); + + + userService.registerUser(new RegisterRequestDTO( + "user1", + "email1", + "password1" + )); + userService.registerUser(new RegisterRequestDTO( + "user2", + "email2", + "password2" + )); + + this.user1 = userService.getByEmail("email1"); + this.user2 = userService.getByEmail("email2"); + + this.user1Jwt = + restTestClient.post().uri("/auth/login").contentType(MediaType.APPLICATION_JSON) + .body(Map.of( + "email", + user1.getEmail(), + "rawPassword", + "password1" + )).exchange() + .expectStatus().isOk() + .returnResult().getResponseCookies().getFirst("jwt").getValue(); + + this.user2Jwt = + restTestClient.post().uri("/auth/login").contentType(MediaType.APPLICATION_JSON) + .body(Map.of( + "email", + user2.getEmail(), + "rawPassword", + "password2" + )).exchange() + .expectStatus().isOk() + .returnResult().getResponseCookies().getFirst("jwt").getValue(); + createDraftSession(); + + List transports = List.of(new WebSocketTransport(new StandardWebSocketClient())); + + SockJsClient sockJsClient1 = new SockJsClient(transports); + SockJsClient sockJsClient2 = new SockJsClient(transports); + + var stompClient1 = new WebSocketStompClient(sockJsClient1); + var stompClient2 = new WebSocketStompClient(sockJsClient2); + + stompClient1.setMessageConverter(new JacksonJsonMessageConverter()); + stompClient2.setMessageConverter(new JacksonJsonMessageConverter()); + + WebSocketHttpHeaders httpHeaders1 = new WebSocketHttpHeaders(); + WebSocketHttpHeaders httpHeaders2 = new WebSocketHttpHeaders(); + + httpHeaders1.add( + "Cookie", + "jwt=" + user1Jwt + ); + httpHeaders2.add( + "Cookie", + "jwt=" + user2Jwt + ); + + String url = "ws://localhost:" + port + "/game-ws"; + CompletableFuture completableFuture1 = stompClient1.connectAsync( + url, + httpHeaders1, + new StompHeaders(), + new StompSessionHandlerAdapter() { + } + ); + CompletableFuture completableFuture2 = stompClient2.connectAsync( + url, + httpHeaders2, + new StompHeaders(), + new StompSessionHandlerAdapter() { + } + ); + user1Session = completableFuture1.get( + 5, + TimeUnit.SECONDS + ); + user2Session = completableFuture2.get( + 5, + TimeUnit.SECONDS + ); + + user1Queue = new LinkedBlockingQueue<>(); + user2Queue = new LinkedBlockingQueue<>(); + + user1Session.subscribe( + "/topic/draft-session/" + draftSession.getId(), + new StompSessionHandlerAdapter() { + @Override + public Type getPayloadType(StompHeaders headers) { + return DraftSessionResponseDTO.class; + } + + @Override + public void handleFrame(StompHeaders headers, @Nullable Object payload) { + user1Queue.add((DraftSessionResponseDTO) payload); + } + } + ); + user2Session.subscribe( + "/topic/draft-session/" + draftSession.getId(), + new StompSessionHandlerAdapter() { + @Override + public Type getPayloadType(StompHeaders headers) { + return DraftSessionResponseDTO.class; + } + + @Override + public void handleFrame(StompHeaders headers, @Nullable Object payload) { + user2Queue.add((DraftSessionResponseDTO) payload); + } + } + ); + when(draftSessionSettings.getTimeToPick()).thenReturn(Duration.ofMillis(500)); + } + + @AfterEach + void tearDown() { + draftSessionRepository.deleteAll(); + userRepository.deleteAll(); + + if (user1Session != null) { + user1Session.disconnect(); + } + + if (user2Session != null) { + user2Session.disconnect(); + } + } + + void createDraftSession() { + var draftSession = new DraftSession( + user1.getId(), + user2.getId() + ); + this.draftSession = draftSessionRepository.save(draftSession); + log.info("Draft session id: " + draftSession.getId()); + } + + @Test + void DraftSession_HappyPath() throws InterruptedException { + user1Session.send( + "/app/" + draftSession.getId() + "/ban", + new OptionEntity( + Category.Language, + Language.CPP.name() + ) + ); + + + var res1 = user1Queue.take(); + var res2 = user2Queue.take(); + + assertNotEquals( + null, + res1 + ); + assertNotEquals( + null, + res2 + ); + + assertEquals( + DraftSessionResponseDTO.Status.SUCCEED, + res1.getStatus() + ); + + assertFalse(res1.getDraftSession().getRemainOptions().get(Category.Language) + .contains(Language.CPP.name())); + + assertFalse(res1.getDraftSession().isFirstUserMove); + + user2Session.send( + "/app/" + draftSession.getId() + "/ban", + new OptionEntity( + Category.ProblemType, + ProblemType.Algorithm.name() + ) + ); + + res1 = user1Queue.take(); + + res2 = user2Queue.take(); + + assertNotEquals( + null, + res1 + ); + assertNotEquals( + null, + res2 + ); + + assertFalse(res2.getDraftSession().remainOptions.get(Category.ProblemType) + .contains(ProblemType.Algorithm.name())); + } + + @Test + void DraftSession_OneUserCantBanTwice() throws InterruptedException { + user1Session.send( + "/app/" + draftSession.getId() + "/ban", + new OptionEntity( + Category.Language, + Language.CPP.name() + ) + ); + + var res = user1Queue.poll( + 1, + TimeUnit.SECONDS + ); + assertNotNull(res); + + assertEquals( + DraftSessionResponseDTO.Status.SUCCEED, + res.getStatus() + ); + + user1Session.send( + "/app/" + draftSession.getId() + "/ban", + new OptionEntity( + Category.ProblemType, + ProblemType.Algorithm.name() + ) + ); + + res = user1Queue.poll( + 1, + TimeUnit.SECONDS + ); + assertNotNull(res); + + assertEquals( + DraftSessionResponseDTO.Status.ERROR, + res.getStatus() + ); + + log.info("Error message: " + res.getError()); + } + + @Test + void DraftSession_CantBanWrongOption() throws InterruptedException { + user1Session.send( + "/app/" + draftSession.getId() + "/ban", + new OptionEntity( + Category.Language, + "wrong language" + ) + ); + + var res = user1Queue.poll( + 1, + TimeUnit.SECONDS + ); + assertNotNull(res); + assertEquals( + DraftSessionResponseDTO.Status.ERROR, + res.getStatus(), + "We cant ban not existing option!" + ); + + user1Session.send( + "/app/" + draftSession.getId() + "/ban", + new OptionEntity( + Category.ProblemType, + ProblemType.Algorithm.name() + ) + ); + + res = user1Queue.poll( + 1, + TimeUnit.SECONDS + ); + assertNotNull(res); + + user2Session.send( + "/app/" + draftSession.getId() + "/ban", + new OptionEntity( + Category.ProblemType, + ProblemType.Algorithm.name() + ) + ); + + res = user1Queue.poll( + 1, + TimeUnit.SECONDS + ); + + assertNotNull(res); + + assertEquals( + DraftSessionResponseDTO.Status.ERROR, + res.getStatus(), + "We can ban one option twice!" + ); + + } + + @Test + void DraftSession_CantBanTheLastOptionInCategory() throws InterruptedException { + + log.info("All sessions : " + + draftSessionRepository.findAll().stream().map(DraftSession::getId) + .toList()); + + var categoryOptional = Arrays.stream(Category.values()).findFirst(); + assertTrue( + categoryOptional.isPresent(), + "There is no category at all!" + ); + var category = categoryOptional.get(); + + List allMessages = new ArrayList<>(); + for (var option : category.getEnumClass().getEnumConstants()) { + (draftSession.isFirstUserMove() ? user1Session : user2Session) + .send( + "/app/" + draftSession.getId() + "/ban", + new OptionEntity( + category, + option.name() + ) + ); + draftSession.setFirstUserMove(!draftSession.isFirstUserMove); + log.info( + "{}, {}", + category, + option + ); + allMessages.add(user1Queue.take()); + } + + assertEquals( + DraftSessionResponseDTO.Status.ERROR, + allMessages.getLast().getStatus() + ); + + log.info( + "Error message: {}", + allMessages.getLast().getError() + ); + } + + @Test + void DraftSession_WhenTimeToPickExceededItIsRandomBan() throws InterruptedException { + when(draftSessionSettings.getTimeToPick()).thenReturn(Duration.ofMillis(100)); + user1Session.send( + "/app/" + draftSession.getId() + "/ban", + new OptionEntity( + Category.Language, + Language.CPP.name() + ) + ); + + var res = user1Queue.take(); + assertFalse(res.getDraftSession().isFirstUserMove()); + var optionsRemainAfterFirstBan = res.getDraftSession().getRemainOptions().values() + .stream().map(Set::size) + .reduce( + 0, + Integer::sum + ); + + var resAfterRandomBan = user1Queue.poll( + 110, + TimeUnit.MILLISECONDS + ); + + assertNotNull(resAfterRandomBan); + assertNotNull(resAfterRandomBan.getDraftSession()); + assertTrue(resAfterRandomBan.getDraftSession().isFirstUserMove()); + + var optionsRemainAfterRandomBan = + resAfterRandomBan.getDraftSession().getRemainOptions().values() + .stream().map(Set::size) + .reduce( + 0, + Integer::sum + ); + + assertEquals( + optionsRemainAfterFirstBan - 1, + optionsRemainAfterRandomBan + ); + } + +} \ No newline at end of file diff --git a/src/test/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansControllerTest.java b/src/test/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansControllerTest.java new file mode 100644 index 0000000..f9da466 --- /dev/null +++ b/src/test/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansControllerTest.java @@ -0,0 +1,137 @@ +package com.codzilla.backend.PreMatch.PicksBans; + +import com.codzilla.backend.BaseIntegrationTest; +import com.codzilla.backend.PreMatch.model.Category; +import com.codzilla.backend.PreMatch.model.DraftSessionResponseDTO; +import com.codzilla.backend.PreMatch.model.Language; +import com.codzilla.backend.PreMatch.model.OptionEntity; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.core.io.ResourceLoader; +import org.springframework.http.MediaType; +import org.springframework.messaging.converter.JacksonJsonMessageConverter; +import org.springframework.messaging.simp.stomp.StompHeaders; +import org.springframework.messaging.simp.stomp.StompSession; +import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.web.socket.WebSocketHttpHeaders; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; +import org.springframework.web.socket.messaging.WebSocketStompClient; +import org.springframework.web.socket.sockjs.client.SockJsClient; +import org.springframework.web.socket.sockjs.client.Transport; +import org.springframework.web.socket.sockjs.client.WebSocketTransport; +import org.springframework.boot.resttestclient.TestRestTemplate; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import java.util.concurrent.*; + +import static org.junit.jupiter.api.Assertions.*; + + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class PicksBansControllerTest { + private static final Logger log = LoggerFactory.getLogger(PicksBansControllerTest.class); + @LocalServerPort + private static int port; + + private static WebSocketStompClient stompClient; + + private static RestTestClient restTestClient; + + private static String user1JwtToken; + private static String user2JwtToken; + + @Autowired + private static ResourceLoader resourceLoader; + + @BeforeAll + public static void setup() { + + List transports = List.of(new WebSocketTransport(new StandardWebSocketClient())); + SockJsClient sockJsClient = new SockJsClient(transports); + + stompClient = new WebSocketStompClient(sockJsClient); + stompClient.setMessageConverter(new JacksonJsonMessageConverter()); + + + restTestClient = RestTestClient.bindToServer().baseUrl("http://localhost:"+port).build(); + user1JwtToken = restTestClient.post().uri("/auth/login").contentType(MediaType.APPLICATION_JSON) + .body(Map.of("email", "u@gmail.co", "rawPassword", "0")).exchange() + .expectStatus().isOk() + .returnResult().getResponseCookies().getFirst("jwt").getValue(); + } + + @Test + public void testBanOption() throws Exception { + String url = "ws://localhost:" + port + "/game-ws"; + String lobbyId = "bfe4b2e0-d1bf-40c1-b0b2-d7836ff26c4b"; + + WebSocketHttpHeaders httpHeaders = new WebSocketHttpHeaders(); + + httpHeaders.add( + "Cookie", + "jwt=" + user1JwtToken + ); + + CompletableFuture completableFuture = stompClient.connectAsync( + url, + httpHeaders, + new StompHeaders(), + new StompSessionHandlerAdapter() { + } + ); + + StompSession session = completableFuture.get( + 5, + TimeUnit.SECONDS + ); + + OptionEntity request = new OptionEntity( + Category.Language, + Language.CPP.name() + ); + + BlockingQueue queue = new LinkedBlockingQueue<>(); + session.subscribe( + "/topic/draft-session/" + lobbyId, + new StompSessionHandlerAdapter() { + @Override + public Type getPayloadType(StompHeaders headers) { + return DraftSessionResponseDTO.class; + } + + @Override + public void handleFrame(StompHeaders headers, @Nullable Object payload) { + queue.add((DraftSessionResponseDTO) payload); + } + } + ); + + session.send( + "/app/" + lobbyId + "/ban", + request + ); + + + while (true) { + var res = queue.take(); + if (res.getStatus() == DraftSessionResponseDTO.Status.ERROR) { + log.info("Error: {}", res.getError()); + } else { + log.info("Result: {}", res.getDraftSession().getRemainOptions()); + } + + } +// assertTrue(session.isConnected()); + } +} \ No newline at end of file diff --git a/src/test/java/com/codzilla/backend/S3/S3RepositoryTest.java b/src/test/java/com/codzilla/backend/S3/S3RepositoryTest.java index ff97713..004f3ac 100644 --- a/src/test/java/com/codzilla/backend/S3/S3RepositoryTest.java +++ b/src/test/java/com/codzilla/backend/S3/S3RepositoryTest.java @@ -1,5 +1,6 @@ package com.codzilla.backend.S3; +import com.codzilla.backend.BaseIntegrationTest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -9,7 +10,7 @@ import static org.junit.jupiter.api.Assertions.*; @SpringBootTest -class S3RepositoryTest { +class S3RepositoryTest extends BaseIntegrationTest { @Autowired S3Repository repository; diff --git a/src/test/java/com/codzilla/backend/Sandbox/integration/ProblemIntegrationTest.java b/src/test/java/com/codzilla/backend/Sandbox/integration/ProblemIntegrationTest.java index 0c58003..024526f 100644 --- a/src/test/java/com/codzilla/backend/Sandbox/integration/ProblemIntegrationTest.java +++ b/src/test/java/com/codzilla/backend/Sandbox/integration/ProblemIntegrationTest.java @@ -1,13 +1,18 @@ package com.codzilla.backend.Sandbox.integration; +import com.codzilla.backend.Authentication.JWTRequestFilter.JWTRequestFilter; +import com.codzilla.backend.Authentication.JWTUtils.JWTUtils; import com.codzilla.backend.S3.S3Initialization; import com.codzilla.backend.User.UserRepository; +import com.codzilla.backend.User.UserService; import com.codzilla.backend.controller.Sandbox.judge0.Judge0Client; import com.codzilla.backend.controller.Sandbox.polygon.PolygonClient; import com.codzilla.backend.controller.Sandbox.polygon.PolygonProblem; import com.codzilla.backend.controller.Sandbox.problem.Problem; +import com.codzilla.backend.controller.Sandbox.problem.ProblemController; import com.codzilla.backend.controller.Sandbox.problem.ProblemRepository; +import com.codzilla.backend.controller.Sandbox.problem.ProblemService; import com.codzilla.backend.controller.Sandbox.submission.Submission; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; @@ -15,6 +20,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -27,29 +33,17 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@SpringBootTest( - webEnvironment = SpringBootTest.WebEnvironment.MOCK, - properties = { - "spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1", - "spring.datasource.driver-class-name=org.h2.Driver", - "spring.datasource.username=sa", - "spring.datasource.password=", - "spring.jpa.database-platform=org.hibernate.dialect.H2Dialect", - "spring.jpa.hibernate.ddl-auto=create-drop", - - "app.s3.enabled=false", - - "spring.autoconfigure.exclude=" + - "org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration," + - "org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration" - } -) + @AutoConfigureMockMvc(addFilters = false) +@WebMvcTest(ProblemController.class) class ProblemIntegrationTest { @Autowired private MockMvc mockMvc; + @MockitoBean + JWTUtils jwtUtils; + @MockitoBean private com.codzilla.backend.controller.Sandbox.problem.ProblemTestRepository problemTestRepository; @@ -58,6 +52,7 @@ class ProblemIntegrationTest { @MockitoBean private ProblemRepository problemRepository; + @MockitoBean private S3Initialization s3Initialization; @@ -69,12 +64,19 @@ class ProblemIntegrationTest { @MockitoBean private UserRepository userRepository; + @MockitoBean private com.codzilla.backend.controller.Sandbox.polygon.PolygonProblemService polygonProblemService; @MockitoBean private com.codzilla.backend.controller.Sandbox.submission.SubmissionRepository submissionRepository; + @MockitoBean + UserService userService; + + @MockitoBean + ProblemService problemService; + @Test void fullFlow_createAndSubmit() throws Exception { Problem problem = new Problem(); diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index ad92b53..50273dd 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -3,7 +3,7 @@ spring.application.name=Backend app.security.jwt.access-token-ttl=10s app.security.jwt.refresh-token-ttl=60s -app.s3.endpoint=http://localhost:9000 +#app.s3.endpoint=http://localhost:9000 app.s3.access-key=minio-user app.s3.secret-key=minio-password app.s3.region=us-east-1 @@ -13,11 +13,15 @@ app.s3.bucket-name=codzilla #spring.datasource.username=${DB_USERNAME} #spring.datasource.password=${DB_PASSWORD} -spring.datasource.url=jdbc:postgresql://localhost:5433/testingdb -spring.datasource.username=myuser -spring.datasource.password=secret +app.draft-session.time-to-pick=10s -spring.jpa.hibernate.ddl-auto=update +spring.datasource.url=jdbc:tc:postgresql:16:///testingdb +spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver + +spring.datasource.username=test +spring.datasource.password=test + +spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true spring.data.redis.host=localhost From 0b3e9a689748906d88dc6fde44dc1c1f36a4118e Mon Sep 17 00:00:00 2001 From: toximu Date: Wed, 20 May 2026 13:56:08 +0300 Subject: [PATCH 2/5] delete test test --- .../PicksBans/PicksBansControllerTest.java | 137 ------------------ 1 file changed, 137 deletions(-) delete mode 100644 src/test/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansControllerTest.java diff --git a/src/test/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansControllerTest.java b/src/test/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansControllerTest.java deleted file mode 100644 index f9da466..0000000 --- a/src/test/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansControllerTest.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.codzilla.backend.PreMatch.PicksBans; - -import com.codzilla.backend.BaseIntegrationTest; -import com.codzilla.backend.PreMatch.model.Category; -import com.codzilla.backend.PreMatch.model.DraftSessionResponseDTO; -import com.codzilla.backend.PreMatch.model.Language; -import com.codzilla.backend.PreMatch.model.OptionEntity; -import org.jspecify.annotations.Nullable; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.core.io.ResourceLoader; -import org.springframework.http.MediaType; -import org.springframework.messaging.converter.JacksonJsonMessageConverter; -import org.springframework.messaging.simp.stomp.StompHeaders; -import org.springframework.messaging.simp.stomp.StompSession; -import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; -import org.springframework.test.web.servlet.client.RestTestClient; -import org.springframework.web.socket.WebSocketHttpHeaders; -import org.springframework.web.socket.client.standard.StandardWebSocketClient; -import org.springframework.web.socket.messaging.WebSocketStompClient; -import org.springframework.web.socket.sockjs.client.SockJsClient; -import org.springframework.web.socket.sockjs.client.Transport; -import org.springframework.web.socket.sockjs.client.WebSocketTransport; -import org.springframework.boot.resttestclient.TestRestTemplate; -import java.lang.reflect.Type; -import java.util.List; -import java.util.Map; -import java.util.concurrent.*; - -import static org.junit.jupiter.api.Assertions.*; - - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class PicksBansControllerTest { - private static final Logger log = LoggerFactory.getLogger(PicksBansControllerTest.class); - @LocalServerPort - private static int port; - - private static WebSocketStompClient stompClient; - - private static RestTestClient restTestClient; - - private static String user1JwtToken; - private static String user2JwtToken; - - @Autowired - private static ResourceLoader resourceLoader; - - @BeforeAll - public static void setup() { - - List transports = List.of(new WebSocketTransport(new StandardWebSocketClient())); - SockJsClient sockJsClient = new SockJsClient(transports); - - stompClient = new WebSocketStompClient(sockJsClient); - stompClient.setMessageConverter(new JacksonJsonMessageConverter()); - - - restTestClient = RestTestClient.bindToServer().baseUrl("http://localhost:"+port).build(); - user1JwtToken = restTestClient.post().uri("/auth/login").contentType(MediaType.APPLICATION_JSON) - .body(Map.of("email", "u@gmail.co", "rawPassword", "0")).exchange() - .expectStatus().isOk() - .returnResult().getResponseCookies().getFirst("jwt").getValue(); - } - - @Test - public void testBanOption() throws Exception { - String url = "ws://localhost:" + port + "/game-ws"; - String lobbyId = "bfe4b2e0-d1bf-40c1-b0b2-d7836ff26c4b"; - - WebSocketHttpHeaders httpHeaders = new WebSocketHttpHeaders(); - - httpHeaders.add( - "Cookie", - "jwt=" + user1JwtToken - ); - - CompletableFuture completableFuture = stompClient.connectAsync( - url, - httpHeaders, - new StompHeaders(), - new StompSessionHandlerAdapter() { - } - ); - - StompSession session = completableFuture.get( - 5, - TimeUnit.SECONDS - ); - - OptionEntity request = new OptionEntity( - Category.Language, - Language.CPP.name() - ); - - BlockingQueue queue = new LinkedBlockingQueue<>(); - session.subscribe( - "/topic/draft-session/" + lobbyId, - new StompSessionHandlerAdapter() { - @Override - public Type getPayloadType(StompHeaders headers) { - return DraftSessionResponseDTO.class; - } - - @Override - public void handleFrame(StompHeaders headers, @Nullable Object payload) { - queue.add((DraftSessionResponseDTO) payload); - } - } - ); - - session.send( - "/app/" + lobbyId + "/ban", - request - ); - - - while (true) { - var res = queue.take(); - if (res.getStatus() == DraftSessionResponseDTO.Status.ERROR) { - log.info("Error: {}", res.getError()); - } else { - log.info("Result: {}", res.getDraftSession().getRemainOptions()); - } - - } -// assertTrue(session.isConnected()); - } -} \ No newline at end of file From 3b74dfc6fcd25508a7606ee2059fc8c3a1ad8a44 Mon Sep 17 00:00:00 2001 From: toximu Date: Sat, 23 May 2026 22:06:21 +0300 Subject: [PATCH 3/5] add startDraftSession method --- .../DraftSession/DraftSessionService.java | 15 ++- .../PicksBans/PicksBansController.java | 40 ++++-- .../backend/WebSocket/WebSocketConfig.java | 5 +- .../DraftSession/DraftSessionServiceTest.java | 123 ++++++++++++++---- 4 files changed, 138 insertions(+), 45 deletions(-) diff --git a/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionService.java b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionService.java index 754524c..8308d00 100644 --- a/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionService.java +++ b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionService.java @@ -11,10 +11,7 @@ import org.springframework.transaction.support.TransactionTemplate; import java.time.Instant; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadLocalRandom; @@ -193,4 +190,14 @@ public void executeRandomBan(UUID draftSessionId) { startTimer(draftSessionId); } } + + public Optional findById(UUID id) { + return draftSessionRepository.findById(id); + } + public DraftSession startDraftSession(UUID firstUserId, UUID secondUserId) { + DraftSession draftSession = new DraftSession(firstUserId, secondUserId); + draftSessionRepository.save(draftSession); + startTimer(draftSession.getId()); + return draftSession; + } } diff --git a/src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansController.java b/src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansController.java index fa56adf..cbbc8b0 100644 --- a/src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansController.java +++ b/src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansController.java @@ -12,6 +12,7 @@ import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.messaging.simp.annotation.SubscribeMapping; import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; @@ -41,16 +42,7 @@ void handleBanRequest( @AuthenticationPrincipal Authentication auth, @Payload OptionEntity optionEntity) { User user = (User) auth.getPrincipal(); - log.info("In ban controller"); - log.info( - "User : {}", - user - ); - log.info( - "Request: {}", - optionEntity.getBanObject() - ); - log.info("All sessions: {}", draftSessionRepository.findAll().stream().map(DraftSession::getId).toList()); + log.info("request: {}", optionEntity.getBanObject()); try { assert user != null; var draftSession = draftSessionService.processBan( @@ -66,13 +58,19 @@ void handleBanRequest( null ) ); + log.info("USER: {}, isFirst {}", user.getUsername(), draftSession.isFirstUserMove()); } catch (DraftSessionException exception) { log.info( "Exception: {}", exception.toString() ); - messagingTemplate.convertAndSend( - "/topic/draft-session/" + draftSessionId, + log.info( + "User Username: {}", + user.getUsername() + ); + messagingTemplate.convertAndSendToUser( + user.getUsername(), + "/queue/errors", new DraftSessionResponseDTO( DraftSessionResponseDTO.Status.ERROR, null, @@ -81,4 +79,22 @@ void handleBanRequest( ); } } + + @SubscribeMapping("/draft-session/{draftSessionId}") + public DraftSessionResponseDTO getInitialState(@DestinationVariable UUID draftSessionId) { + + + var draftSession = draftSessionService.findById(draftSessionId); + log.info("Subscribed."); + if (draftSession.isEmpty()) { + return new DraftSessionResponseDTO(DraftSessionResponseDTO.Status.ERROR, null, + "Incorrect session id"); + } + log.info("Subscribed. Draft session id: {}", draftSession.get().getId()); + return new DraftSessionResponseDTO( + DraftSessionResponseDTO.Status.SUCCEED, + draftSession.get(), + null + ); + } } diff --git a/src/main/java/com/codzilla/backend/WebSocket/WebSocketConfig.java b/src/main/java/com/codzilla/backend/WebSocket/WebSocketConfig.java index 2ced0d8..2c98baa 100644 --- a/src/main/java/com/codzilla/backend/WebSocket/WebSocketConfig.java +++ b/src/main/java/com/codzilla/backend/WebSocket/WebSocketConfig.java @@ -35,8 +35,9 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { - config.enableSimpleBroker("/topic"); - config.setApplicationDestinationPrefixes("/app"); + config.enableSimpleBroker("/topic", "/queue"); + config.setApplicationDestinationPrefixes("/app", "/topic"); + config.setUserDestinationPrefix("/user"); } @Override diff --git a/src/test/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionServiceTest.java b/src/test/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionServiceTest.java index 20936ae..32054f3 100644 --- a/src/test/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionServiceTest.java +++ b/src/test/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionServiceTest.java @@ -78,6 +78,8 @@ class DraftSessionServiceTest extends BaseIntegrationTest { private DraftSession draftSession; @Autowired private WebContentGenerator webContentGenerator; + @Autowired + private DraftSessionService draftSessionService; @BeforeEach void setup() throws Exception { @@ -119,6 +121,7 @@ void setup() throws Exception { )).exchange() .expectStatus().isOk() .returnResult().getResponseCookies().getFirst("jwt").getValue(); +// when(draftSessionSettings.getTimeToPick()).thenReturn(Duration.ofMillis(50000)); createDraftSession(); List transports = List.of(new WebSocketTransport(new StandardWebSocketClient())); @@ -185,6 +188,20 @@ public void handleFrame(StompHeaders headers, @Nullable Object payload) { } } ); + user1Session.subscribe( + "/user/queue/errors", + new StompSessionHandlerAdapter() { + @Override + public Type getPayloadType(StompHeaders headers) { + return DraftSessionResponseDTO.class; + } + + @Override + public void handleFrame(StompHeaders headers, @Nullable Object payload) { + user1Queue.add((DraftSessionResponseDTO) payload); + } + } + ); user2Session.subscribe( "/topic/draft-session/" + draftSession.getId(), new StompSessionHandlerAdapter() { @@ -199,7 +216,23 @@ public void handleFrame(StompHeaders headers, @Nullable Object payload) { } } ); - when(draftSessionSettings.getTimeToPick()).thenReturn(Duration.ofMillis(500)); + user2Session.subscribe( + "/user/queue/errors", + new StompSessionHandlerAdapter() { + @Override + public Type getPayloadType(StompHeaders headers) { + return DraftSessionResponseDTO.class; + } + + @Override + public void handleFrame(StompHeaders headers, @Nullable Object payload) { + user2Queue.add((DraftSessionResponseDTO) payload); + } + } + ); + user1Queue.take(); + user2Queue.take(); + } @AfterEach @@ -217,11 +250,10 @@ void tearDown() { } void createDraftSession() { - var draftSession = new DraftSession( + this.draftSession = draftSessionService.startDraftSession( user1.getId(), user2.getId() ); - this.draftSession = draftSessionRepository.save(draftSession); log.info("Draft session id: " + draftSession.getId()); } @@ -293,16 +325,19 @@ void DraftSession_OneUserCantBanTwice() throws InterruptedException { ) ); - var res = user1Queue.poll( - 1, - TimeUnit.SECONDS - ); + var res = user1Queue.take(); assertNotNull(res); assertEquals( DraftSessionResponseDTO.Status.SUCCEED, res.getStatus() ); + assertFalse(res.getDraftSession().getRemainOptions().get(Category.Language) + .contains(Language.CPP.name())); + log.info( + "FIRST: {}", + res.getDraftSession().getRemainOptions() + ); user1Session.send( "/app/" + draftSession.getId() + "/ban", @@ -317,13 +352,11 @@ void DraftSession_OneUserCantBanTwice() throws InterruptedException { TimeUnit.SECONDS ); assertNotNull(res); - assertEquals( DraftSessionResponseDTO.Status.ERROR, res.getStatus() ); - log.info("Error message: " + res.getError()); } @Test @@ -359,8 +392,15 @@ void DraftSession_CantBanWrongOption() throws InterruptedException { 1, TimeUnit.SECONDS ); + user2Queue.poll( + 1, + TimeUnit.SECONDS + ); assertNotNull(res); - + assertEquals( + DraftSessionResponseDTO.Status.SUCCEED, + res.getStatus() + ); user2Session.send( "/app/" + draftSession.getId() + "/ban", new OptionEntity( @@ -369,7 +409,7 @@ void DraftSession_CantBanWrongOption() throws InterruptedException { ) ); - res = user1Queue.poll( + res = user2Queue.poll( 1, TimeUnit.SECONDS ); @@ -379,7 +419,7 @@ void DraftSession_CantBanWrongOption() throws InterruptedException { assertEquals( DraftSessionResponseDTO.Status.ERROR, res.getStatus(), - "We can ban one option twice!" + "We cant ban one option twice!" ); } @@ -387,10 +427,6 @@ void DraftSession_CantBanWrongOption() throws InterruptedException { @Test void DraftSession_CantBanTheLastOptionInCategory() throws InterruptedException { - log.info("All sessions : " + - draftSessionRepository.findAll().stream().map(DraftSession::getId) - .toList()); - var categoryOptional = Arrays.stream(Category.values()).findFirst(); assertTrue( categoryOptional.isPresent(), @@ -398,7 +434,8 @@ void DraftSession_CantBanTheLastOptionInCategory() throws InterruptedException { ); var category = categoryOptional.get(); - List allMessages = new ArrayList<>(); + List user1Messages = new ArrayList<>(); + List user2Messages = new ArrayList<>(); for (var option : category.getEnumClass().getEnumConstants()) { (draftSession.isFirstUserMove() ? user1Session : user2Session) .send( @@ -408,23 +445,33 @@ void DraftSession_CantBanTheLastOptionInCategory() throws InterruptedException { option.name() ) ); - draftSession.setFirstUserMove(!draftSession.isFirstUserMove); + log.info( "{}, {}", category, option ); - allMessages.add(user1Queue.take()); - } + var res1 = user1Queue.poll( + 1, + TimeUnit.SECONDS + ); + var res2 = user2Queue.poll( + 1, + TimeUnit.SECONDS + ); + if (res1 != null) { + user1Messages.add(res1); + } + if (res2 != null) { + user2Messages.add(res2); + } - assertEquals( - DraftSessionResponseDTO.Status.ERROR, - allMessages.getLast().getStatus() - ); + draftSession.setFirstUserMove(!draftSession.isFirstUserMove); + } - log.info( - "Error message: {}", - allMessages.getLast().getError() + assertTrue( + user1Messages.getLast().getStatus() == DraftSessionResponseDTO.Status.ERROR || + user2Messages.getLast().getStatus() == DraftSessionResponseDTO.Status.ERROR ); } @@ -471,4 +518,26 @@ void DraftSession_WhenTimeToPickExceededItIsRandomBan() throws InterruptedExcept ); } +// @Test +// void fullAutoPickSession() throws InterruptedException { +// when(draftSessionSettings.getTimeToPick()).thenReturn(Duration.ofMillis(100)); +// Thread.sleep(5000); +// DraftSessionResponseDTO last = null; +// while (!user1Queue.isEmpty()) { +// last = user1Queue.take(); +// } +// assertNotNull(last); +// assertEquals( +// DraftSessionResponseDTO.Status.SUCCEED, +// last.getStatus() +// ); +// assertEquals( +// DraftSession.Status.PICKING, +// last.getDraftSession().getStatus() +// ); +// +// assertTrue(last.getDraftSession().getRemainOptions().values().stream() +// .allMatch((options) -> options.size() == 1)); +// } + } \ No newline at end of file From 773898b9e5a4e1bbee60520f018db59cd7b4bc3d Mon Sep 17 00:00:00 2001 From: toximu Date: Sat, 23 May 2026 22:24:32 +0300 Subject: [PATCH 4/5] fix pick time --- .../PreMatch/DraftSession/DraftSessionServiceTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionServiceTest.java b/src/test/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionServiceTest.java index 32054f3..bf14778 100644 --- a/src/test/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionServiceTest.java +++ b/src/test/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionServiceTest.java @@ -121,7 +121,7 @@ void setup() throws Exception { )).exchange() .expectStatus().isOk() .returnResult().getResponseCookies().getFirst("jwt").getValue(); -// when(draftSessionSettings.getTimeToPick()).thenReturn(Duration.ofMillis(50000)); + when(draftSessionSettings.getTimeToPick()).thenReturn(Duration.ofMillis(50000)); createDraftSession(); List transports = List.of(new WebSocketTransport(new StandardWebSocketClient())); @@ -268,8 +268,8 @@ void DraftSession_HappyPath() throws InterruptedException { ); - var res1 = user1Queue.take(); - var res2 = user2Queue.take(); + var res1 = user1Queue.poll(1, TimeUnit.SECONDS); + var res2 = user2Queue.poll(1, TimeUnit.SECONDS); assertNotEquals( null, From 591735d95e43a9697747b31560aa459bef40c1d9 Mon Sep 17 00:00:00 2001 From: toximu Date: Tue, 26 May 2026 12:16:31 +0300 Subject: [PATCH 5/5] refactor + Match Entity --- .../DraftSessionResponseDTO.java | 9 +- .../backend/PreMatch/DTO/ErrorDTO.java | 12 + .../backend/PreMatch/DTO/WebSocketDTO.java | 24 + .../PreMatch/DraftSession/DraftSession.java | 4 +- .../DraftSession/DraftSessionConfig.java | 16 + .../DraftSession/DraftSessionController.java | 69 +++ .../DraftSession/DraftSessionService.java | 68 ++- .../DraftSession/DraftSessionSettings.java | 16 - .../PreMatch/MatchDeliveryListener.java | 53 ++ .../backend/PreMatch/MatchRoom/Match.java | 54 ++ .../PreMatch/MatchRoom/MatchRepository.java | 8 + .../PreMatch/MatchRoom/MatchService.java | 48 ++ .../backend/PreMatch/MatchSettings.java | 22 + .../PicksBans/JsonToMapOptionsConverter.java | 33 -- .../PreMatch/PicksBans/PicksBansConfig.java | 25 - .../PicksBans/PicksBansController.java | 100 ---- .../PreMatch/WebSocketExceptionHandler.java | 43 ++ .../events/DraftSessionFinishedEvent.java | 13 + .../events/DraftSessionUpdatedEvent.java | 13 + .../exceptions/DraftSessionException.java | 14 + .../PreMatch/exceptions/MatchException.java | 12 + .../PreMatch/model/DraftSessionException.java | 7 - src/main/resources/application.properties | 3 +- .../DraftSession/DraftSessionServiceTest.java | 543 ------------------ 24 files changed, 443 insertions(+), 766 deletions(-) rename src/main/java/com/codzilla/backend/PreMatch/{model => DTO}/DraftSessionResponseDTO.java (63%) create mode 100644 src/main/java/com/codzilla/backend/PreMatch/DTO/ErrorDTO.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/DTO/WebSocketDTO.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionConfig.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionController.java delete mode 100644 src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionSettings.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/MatchDeliveryListener.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/MatchRoom/Match.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/MatchRoom/MatchRepository.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/MatchRoom/MatchService.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/MatchSettings.java delete mode 100644 src/main/java/com/codzilla/backend/PreMatch/PicksBans/JsonToMapOptionsConverter.java delete mode 100644 src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansConfig.java delete mode 100644 src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansController.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/WebSocketExceptionHandler.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/events/DraftSessionFinishedEvent.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/events/DraftSessionUpdatedEvent.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/exceptions/DraftSessionException.java create mode 100644 src/main/java/com/codzilla/backend/PreMatch/exceptions/MatchException.java delete mode 100644 src/main/java/com/codzilla/backend/PreMatch/model/DraftSessionException.java delete mode 100644 src/test/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionServiceTest.java diff --git a/src/main/java/com/codzilla/backend/PreMatch/model/DraftSessionResponseDTO.java b/src/main/java/com/codzilla/backend/PreMatch/DTO/DraftSessionResponseDTO.java similarity index 63% rename from src/main/java/com/codzilla/backend/PreMatch/model/DraftSessionResponseDTO.java rename to src/main/java/com/codzilla/backend/PreMatch/DTO/DraftSessionResponseDTO.java index 1f29732..be74d60 100644 --- a/src/main/java/com/codzilla/backend/PreMatch/model/DraftSessionResponseDTO.java +++ b/src/main/java/com/codzilla/backend/PreMatch/DTO/DraftSessionResponseDTO.java @@ -1,4 +1,4 @@ -package com.codzilla.backend.PreMatch.model; +package com.codzilla.backend.PreMatch.DTO; import com.codzilla.backend.PreMatch.DraftSession.DraftSession; import lombok.AllArgsConstructor; @@ -10,12 +10,5 @@ @Getter @Setter public class DraftSessionResponseDTO { - Status status; DraftSession draftSession; - String error; - - public enum Status { - ERROR, - SUCCEED - } } diff --git a/src/main/java/com/codzilla/backend/PreMatch/DTO/ErrorDTO.java b/src/main/java/com/codzilla/backend/PreMatch/DTO/ErrorDTO.java new file mode 100644 index 0000000..cc9f272 --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/DTO/ErrorDTO.java @@ -0,0 +1,12 @@ +package com.codzilla.backend.PreMatch.DTO; + + +public record ErrorDTO( + ErrorStage stage, + String message +) { + public enum ErrorStage { + DRAFT_ERROR, + MATCH_ERROR + } +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/DTO/WebSocketDTO.java b/src/main/java/com/codzilla/backend/PreMatch/DTO/WebSocketDTO.java new file mode 100644 index 0000000..dc0bc7c --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/DTO/WebSocketDTO.java @@ -0,0 +1,24 @@ +package com.codzilla.backend.PreMatch.DTO; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class WebSocketDTO { + public enum Status { + MATCH_STARTED_REDIRECT, + DRAFT, + LIVE + } + + public WebSocketDTO(Status status, Object payload) { + this.status = status; + this.payload = payload; + } + + Status status; + Object payload; +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSession.java b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSession.java index f5300b5..69f8199 100644 --- a/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSession.java +++ b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSession.java @@ -17,7 +17,6 @@ public class DraftSession { @Id - @GeneratedValue(strategy = GenerationType.UUID) @JdbcTypeCode(SqlTypes.UUID) UUID id; @@ -46,10 +45,11 @@ public DraftSession() { } - public DraftSession(UUID firstUserId, UUID secondUserId) { + public DraftSession(UUID matchId, UUID firstUserId, UUID secondUserId) { this(); this.firstUserId = firstUserId; this.secondUserId = secondUserId; + this.id = matchId; } public enum Status { diff --git a/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionConfig.java b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionConfig.java new file mode 100644 index 0000000..d74c1b0 --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionConfig.java @@ -0,0 +1,16 @@ +package com.codzilla.backend.PreMatch.DraftSession; + + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +@Configuration +public class DraftSessionConfig { + + @Bean + public ThreadPoolTaskScheduler threadPoolTaskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + return scheduler; + } +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionController.java b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionController.java new file mode 100644 index 0000000..ed691bd --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionController.java @@ -0,0 +1,69 @@ +package com.codzilla.backend.PreMatch.DraftSession; + +import com.codzilla.backend.PreMatch.DTO.DraftSessionResponseDTO; +import com.codzilla.backend.PreMatch.DTO.WebSocketDTO; +import com.codzilla.backend.PreMatch.MatchSettings; +import com.codzilla.backend.PreMatch.model.OptionEntity; +import com.codzilla.backend.PreMatch.exceptions.DraftSessionException; +import com.codzilla.backend.User.User; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.messaging.simp.annotation.SubscribeMapping; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.stereotype.Controller; + +import java.util.UUID; + +@Slf4j +@Controller +public class DraftSessionController { + + public DraftSessionController(SimpMessagingTemplate messagingTemplate, + DraftSessionService draftSessionService, + MatchSettings matchSettings) { + this.draftSessionService = draftSessionService; + this.messagingTemplate = messagingTemplate; + this.matchSettings = matchSettings; + } + + DraftSessionService draftSessionService; + SimpMessagingTemplate messagingTemplate; + private final MatchSettings matchSettings; + + @MessageMapping("{matchId}/ban") + void handleBanRequest( + @DestinationVariable String matchId, + @AuthenticationPrincipal Authentication auth, + @Payload OptionEntity optionEntity) { + User user = (User) auth.getPrincipal(); + assert user != null; + var draftSession = draftSessionService.processBan( + user, + UUID.fromString(matchId), + optionEntity + ); + + } + + @SubscribeMapping("/match/{matchId}") + public WebSocketDTO getInitialState(@DestinationVariable UUID matchId) { + + + var draftSession = draftSessionService.findById(matchId); + log.info("Subscribed."); + if (draftSession.isEmpty()) { + return null; + } + return new WebSocketDTO( + WebSocketDTO.Status.DRAFT, + new DraftSessionResponseDTO( + draftSession.get() + ) + ); + + } +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionService.java b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionService.java index 8308d00..e305c16 100644 --- a/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionService.java +++ b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionService.java @@ -1,10 +1,17 @@ package com.codzilla.backend.PreMatch.DraftSession; +import com.codzilla.backend.PreMatch.DTO.DraftSessionResponseDTO; +import com.codzilla.backend.PreMatch.DTO.WebSocketDTO; +import com.codzilla.backend.PreMatch.MatchSettings; +import com.codzilla.backend.PreMatch.events.DraftSessionFinishedEvent; +import com.codzilla.backend.PreMatch.events.DraftSessionUpdatedEvent; +import com.codzilla.backend.PreMatch.exceptions.DraftSessionException; import com.codzilla.backend.PreMatch.model.*; import com.codzilla.backend.User.User; import jakarta.transaction.Transactional; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; @@ -22,24 +29,23 @@ public class DraftSessionService { public DraftSessionService(DraftSessionRepository draftSessionRepository, @Qualifier("threadPoolTaskScheduler") TaskScheduler taskScheduler, - SimpMessagingTemplate messagingTemplate, TransactionTemplate transactionTemplate, - DraftSessionSettings settings + MatchSettings matchSettings, + ApplicationEventPublisher eventPublisher ) { this.draftSessionRepository = draftSessionRepository; this.taskScheduler = taskScheduler; - this.messagingTemplate = messagingTemplate; this.transactionTemplate = transactionTemplate; - this.settings = settings; + this.matchSettings = matchSettings; + this.eventPublisher = eventPublisher; } private final DraftSessionRepository draftSessionRepository; private final TaskScheduler taskScheduler; - private final SimpMessagingTemplate messagingTemplate; private final TransactionTemplate transactionTemplate; - private final DraftSessionSettings settings; - + private final MatchSettings matchSettings; + private final ApplicationEventPublisher eventPublisher; private final Map> scheduledTasks = new ConcurrentHashMap<>(); @Transactional @@ -48,14 +54,12 @@ public DraftSession processBan(User user, UUID draftSessionId, OptionEntity opti () -> new RuntimeException( "There is no lobby: " + draftSessionId)); - cancelTimer(draftSessionId); - var nowUserMoving = draftSession.isFirstUserMove ? draftSession.getFirstUserId() : draftSession.getSecondUserId(); if (!nowUserMoving.equals(user.getId())) { - throw new DraftSessionException("It is not your turn now!"); + throw new DraftSessionException(DraftSessionException.DraftErrorType.NOT_YOUR_TURN); } banOption( @@ -65,10 +69,11 @@ public DraftSession processBan(User user, UUID draftSessionId, OptionEntity opti if (draftSession.getStatus().equals(DraftSession.Status.PICKING)) { startTimer(draftSessionId); + draftSessionRepository.save(draftSession); + } else if (draftSession.getStatus().equals(DraftSession.Status.FINISHED)) { + draftSessionRepository.deleteById(draftSessionId); } - draftSessionRepository.save(draftSession); - return draftSession; } @@ -83,24 +88,27 @@ private void banOption(DraftSession draftSession, OptionEntity optionEntity) { if (!isOptionExists) { - throw new DraftSessionException("Invalid ban option!"); + throw new DraftSessionException(DraftSessionException.DraftErrorType.OPTION_DO_NOT_EXISTS); } long optionsOfCategoryRemain = draftSession.remainOptions.get(optionEntity.getCategory()).size(); if (optionsOfCategoryRemain == 1) { - throw new DraftSessionException("There is only one object in this category! You cant " + - "ban it."); + throw new DraftSessionException(DraftSessionException.DraftErrorType.CAN_NOT_BAN_LAST_OPTION); } if (!draftSession.remainOptions.get(optionEntity.getCategory()) .contains(optionEntity.getBanObject())) { - throw new DraftSessionException("Already banned: " + optionEntity.getBanObject()); + throw new DraftSessionException(DraftSessionException.DraftErrorType.OPTION_ALREADY_BANNED); } - + cancelTimer(draftSession.getId()); draftSession.remainOptions.get(optionEntity.getCategory()) .remove(optionEntity.getBanObject()); + log.info( + "BAN OPTION: {}", + optionEntity + ); draftSession.setFirstUserMove(!draftSession.isFirstUserMove()); boolean isDraftSessionFinished = @@ -110,6 +118,9 @@ private void banOption(DraftSession draftSession, OptionEntity optionEntity) { if (isDraftSessionFinished) { draftSession.setStatus(DraftSession.Status.FINISHED); + eventPublisher.publishEvent(new DraftSessionFinishedEvent(draftSession)); + } else { + eventPublisher.publishEvent(new DraftSessionUpdatedEvent(draftSession)); } } @@ -129,7 +140,7 @@ private void startTimer(UUID draftSessionId) { ); } }, - Instant.now().plus(settings.getTimeToPick()) + Instant.now().plus(matchSettings.getTimeToPick()) ); scheduledTasks.put( draftSessionId, @@ -144,8 +155,8 @@ private void cancelTimer(UUID draftSessionId) { } } - @Transactional public void executeRandomBan(UUID draftSessionId) { + DraftSession draftSession = draftSessionRepository.findByIdWithLock(draftSessionId).orElseThrow(); @@ -169,7 +180,6 @@ public void executeRandomBan(UUID draftSessionId) { draftSession, allOptions.get(randomIndex) ); - log.info("BANNED"); break; } catch (RuntimeException _) { } @@ -177,15 +187,6 @@ public void executeRandomBan(UUID draftSessionId) { draftSessionRepository.save(draftSession); - - messagingTemplate.convertAndSend( - "/topic/draft-session/" + draftSessionId, - new DraftSessionResponseDTO( - DraftSessionResponseDTO.Status.SUCCEED, - draftSession, - null - ) - ); if (draftSession.getStatus().equals(DraftSession.Status.PICKING)) { startTimer(draftSessionId); } @@ -194,8 +195,13 @@ public void executeRandomBan(UUID draftSessionId) { public Optional findById(UUID id) { return draftSessionRepository.findById(id); } - public DraftSession startDraftSession(UUID firstUserId, UUID secondUserId) { - DraftSession draftSession = new DraftSession(firstUserId, secondUserId); + + public DraftSession startDraftSession(UUID matchId, UUID firstUserId, UUID secondUserId) { + DraftSession draftSession = new DraftSession( + matchId, + firstUserId, + secondUserId + ); draftSessionRepository.save(draftSession); startTimer(draftSession.getId()); return draftSession; diff --git a/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionSettings.java b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionSettings.java deleted file mode 100644 index 8a2cac7..0000000 --- a/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionSettings.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.codzilla.backend.PreMatch.DraftSession; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -import java.time.Duration; - -@Configuration -@ConfigurationProperties(prefix = "app.draft-session") -@Getter -@Setter -public class DraftSessionSettings { - Duration timeToPick; -} diff --git a/src/main/java/com/codzilla/backend/PreMatch/MatchDeliveryListener.java b/src/main/java/com/codzilla/backend/PreMatch/MatchDeliveryListener.java new file mode 100644 index 0000000..6e38430 --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/MatchDeliveryListener.java @@ -0,0 +1,53 @@ +package com.codzilla.backend.PreMatch; + +import com.codzilla.backend.PreMatch.DTO.DraftSessionResponseDTO; +import com.codzilla.backend.PreMatch.DTO.WebSocketDTO; +import com.codzilla.backend.PreMatch.MatchRoom.Match; +import com.codzilla.backend.PreMatch.MatchRoom.MatchService; +import com.codzilla.backend.PreMatch.events.DraftSessionFinishedEvent; +import com.codzilla.backend.PreMatch.events.DraftSessionUpdatedEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@RequiredArgsConstructor +@Component +public class MatchDeliveryListener { + + private final SimpMessagingTemplate messagingTemplate; + private final MatchSettings matchSettings; + private final MatchService matchService; + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleDraftUpdate(DraftSessionUpdatedEvent event) { + messagingTemplate.convertAndSend( + matchSettings.getWebSocketMatchDestination(event.getDraftSession().getId()), + new WebSocketDTO( + WebSocketDTO.Status.DRAFT, + new DraftSessionResponseDTO( + event.getDraftSession() + ) + ) + ); + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleDraftSessionFinished(DraftSessionFinishedEvent event) { + matchService.setOptionsOfDraftSession( + event.getDraftSession().getId(), + event.getDraftSession() + ); + messagingTemplate.convertAndSend( + matchSettings.getWebSocketMatchDestination(event.getDraftSession().getId()), + new WebSocketDTO( + WebSocketDTO.Status.MATCH_STARTED_REDIRECT, + null + ) + ); + } +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/MatchRoom/Match.java b/src/main/java/com/codzilla/backend/PreMatch/MatchRoom/Match.java new file mode 100644 index 0000000..098acec --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/MatchRoom/Match.java @@ -0,0 +1,54 @@ +package com.codzilla.backend.PreMatch.MatchRoom; + +import com.codzilla.backend.PreMatch.DraftSession.DraftSession; +import com.codzilla.backend.PreMatch.model.Category; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.util.HashMap; +import java.util.UUID; + +@Entity +@Getter +@Setter +public class Match { + + enum Status { + DRAFTING, + LIVE + } + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @JdbcTypeCode(SqlTypes.UUID) + UUID id; + + @JdbcTypeCode(SqlTypes.UUID) + UUID firstUserId; + + @JdbcTypeCode(SqlTypes.UUID) + UUID secondUserId; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "options", columnDefinition = "jsonb") + HashMap options = new HashMap<>(); + + Status status; + + void setOptionsOfDraftSession(DraftSession draftSession) { + + for (var option : draftSession.getRemainOptions().entrySet()) { + options.put(option.getKey(), option.getValue().stream().findFirst().get()); + } + } + + Match(UUID firstUserId, UUID secondUserId) { + this.firstUserId = firstUserId; + this.secondUserId = secondUserId; + } + + public Match() {} +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/MatchRoom/MatchRepository.java b/src/main/java/com/codzilla/backend/PreMatch/MatchRoom/MatchRepository.java new file mode 100644 index 0000000..d9c340a --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/MatchRoom/MatchRepository.java @@ -0,0 +1,8 @@ +package com.codzilla.backend.PreMatch.MatchRoom; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface MatchRepository extends JpaRepository { +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/MatchRoom/MatchService.java b/src/main/java/com/codzilla/backend/PreMatch/MatchRoom/MatchService.java new file mode 100644 index 0000000..099d9d9 --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/MatchRoom/MatchService.java @@ -0,0 +1,48 @@ +package com.codzilla.backend.PreMatch.MatchRoom; + + +import com.codzilla.backend.PreMatch.DTO.WebSocketDTO; +import com.codzilla.backend.PreMatch.DraftSession.DraftSession; +import com.codzilla.backend.PreMatch.DraftSession.DraftSessionService; +import com.codzilla.backend.PreMatch.MatchSettings; +import com.codzilla.backend.PreMatch.events.DraftSessionFinishedEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class MatchService { + + private final MatchRepository matchRepository; + private final DraftSessionService draftSessionService; + private final SimpMessagingTemplate messagingTemplate; + private final MatchSettings matchSettings; + + + public UUID startMatch(UUID firstUserId, UUID secondUserId) { + Match match = new Match( + firstUserId, + secondUserId + ); + match.setStatus(Match.Status.DRAFTING); + matchRepository.save(match); + draftSessionService.startDraftSession( + match.getId(), + firstUserId, + secondUserId + ); + return match.getId(); + } + + public void setOptionsOfDraftSession(UUID matchId, DraftSession draftSession) { + Match match = matchRepository.findById(matchId).get(); + match.setOptionsOfDraftSession(draftSession); + matchRepository.save(match); + } +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/MatchSettings.java b/src/main/java/com/codzilla/backend/PreMatch/MatchSettings.java new file mode 100644 index 0000000..31bb2ec --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/MatchSettings.java @@ -0,0 +1,22 @@ +package com.codzilla.backend.PreMatch; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; +import java.util.UUID; + +@Configuration +@ConfigurationProperties(prefix = "app.match") +@Getter +@Setter +public class MatchSettings { + Duration timeToPick; + String websocketMatchPrefix; + + String getWebSocketMatchDestination(UUID matchId) { + return websocketMatchPrefix + matchId; + } +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/PicksBans/JsonToMapOptionsConverter.java b/src/main/java/com/codzilla/backend/PreMatch/PicksBans/JsonToMapOptionsConverter.java deleted file mode 100644 index 4becc49..0000000 --- a/src/main/java/com/codzilla/backend/PreMatch/PicksBans/JsonToMapOptionsConverter.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.codzilla.backend.infrastructure; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.persistence.AttributeConverter; -import jakarta.persistence.Converter; -import com.codzilla.backend.PreMatch.model.Category; -import java.util.*; - -@Converter -public class JsonToMapOptionsConverter implements AttributeConverter>, String> { - - private final static ObjectMapper objectMapper = new ObjectMapper(); - - @Override - public String convertToDatabaseColumn(Map> attribute) { - try { - return objectMapper.writeValueAsString(attribute); - } catch (JsonProcessingException e) { - return null; - } - } - - @Override - public Map> convertToEntityAttribute(String dbData) { - try { - return objectMapper.readValue(dbData, new TypeReference>>() {}); - } catch (JsonProcessingException e) { - return new HashMap<>(); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansConfig.java b/src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansConfig.java deleted file mode 100644 index aac2bc6..0000000 --- a/src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansConfig.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.codzilla.backend.PreMatch.PicksBans; - - -import com.codzilla.backend.PreMatch.model.Category; -import com.codzilla.backend.PreMatch.model.Language; -import com.codzilla.backend.PreMatch.model.ProblemLevel; -import com.codzilla.backend.PreMatch.model.ProblemType; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -@Configuration -public class PicksBansConfig { - - @Bean - public ThreadPoolTaskScheduler threadPoolTaskScheduler() { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - return scheduler; - } -} diff --git a/src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansController.java b/src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansController.java deleted file mode 100644 index cbbc8b0..0000000 --- a/src/main/java/com/codzilla/backend/PreMatch/PicksBans/PicksBansController.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.codzilla.backend.PreMatch.PicksBans; - -import com.codzilla.backend.PreMatch.DraftSession.DraftSession; -import com.codzilla.backend.PreMatch.DraftSession.DraftSessionRepository; -import com.codzilla.backend.PreMatch.DraftSession.DraftSessionService; -import com.codzilla.backend.PreMatch.model.DraftSessionResponseDTO; -import com.codzilla.backend.PreMatch.model.OptionEntity; -import com.codzilla.backend.PreMatch.model.DraftSessionException; -import com.codzilla.backend.User.User; -import lombok.extern.slf4j.Slf4j; -import org.springframework.messaging.handler.annotation.DestinationVariable; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.messaging.simp.annotation.SubscribeMapping; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.stereotype.Controller; - -import java.security.Principal; -import java.util.UUID; - -@Slf4j -@Controller -public class PicksBansController { - private final DraftSessionRepository draftSessionRepository; - - public PicksBansController(SimpMessagingTemplate messagingTemplate, - DraftSessionService draftSessionService, - DraftSessionRepository draftSessionRepository) { - this.draftSessionService = draftSessionService; - this.messagingTemplate = messagingTemplate; - this.draftSessionRepository = draftSessionRepository; - } - - DraftSessionService draftSessionService; - SimpMessagingTemplate messagingTemplate; - - @MessageMapping("{draftSessionId}/ban") - void handleBanRequest( - @DestinationVariable String draftSessionId, - @AuthenticationPrincipal Authentication auth, - @Payload OptionEntity optionEntity) { - User user = (User) auth.getPrincipal(); - log.info("request: {}", optionEntity.getBanObject()); - try { - assert user != null; - var draftSession = draftSessionService.processBan( - user, - UUID.fromString(draftSessionId), - optionEntity - ); - messagingTemplate.convertAndSend( - "/topic/draft-session/" + draftSessionId, - new DraftSessionResponseDTO( - DraftSessionResponseDTO.Status.SUCCEED, - draftSession, - null - ) - ); - log.info("USER: {}, isFirst {}", user.getUsername(), draftSession.isFirstUserMove()); - } catch (DraftSessionException exception) { - log.info( - "Exception: {}", - exception.toString() - ); - log.info( - "User Username: {}", - user.getUsername() - ); - messagingTemplate.convertAndSendToUser( - user.getUsername(), - "/queue/errors", - new DraftSessionResponseDTO( - DraftSessionResponseDTO.Status.ERROR, - null, - exception.getMessage() - ) - ); - } - } - - @SubscribeMapping("/draft-session/{draftSessionId}") - public DraftSessionResponseDTO getInitialState(@DestinationVariable UUID draftSessionId) { - - - var draftSession = draftSessionService.findById(draftSessionId); - log.info("Subscribed."); - if (draftSession.isEmpty()) { - return new DraftSessionResponseDTO(DraftSessionResponseDTO.Status.ERROR, null, - "Incorrect session id"); - } - log.info("Subscribed. Draft session id: {}", draftSession.get().getId()); - return new DraftSessionResponseDTO( - DraftSessionResponseDTO.Status.SUCCEED, - draftSession.get(), - null - ); - } -} diff --git a/src/main/java/com/codzilla/backend/PreMatch/WebSocketExceptionHandler.java b/src/main/java/com/codzilla/backend/PreMatch/WebSocketExceptionHandler.java new file mode 100644 index 0000000..ccbce4b --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/WebSocketExceptionHandler.java @@ -0,0 +1,43 @@ +package com.codzilla.backend.PreMatch; + +import com.codzilla.backend.PreMatch.DTO.ErrorDTO; +import com.codzilla.backend.PreMatch.exceptions.DraftSessionException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.MessageExceptionHandler; +import org.springframework.messaging.simp.annotation.SendToUser; +import org.springframework.stereotype.Component; +import com.codzilla.backend.PreMatch.exceptions.MatchException; +import org.springframework.web.bind.annotation.ControllerAdvice; + +@ControllerAdvice +@Component +@Slf4j +public class WebSocketExceptionHandler { + @MessageExceptionHandler(DraftSessionException.class) + @SendToUser("/queue/errors") + public ErrorDTO handleDraftException(DraftSessionException ex) { + log.warn( + "Draft error {}", + ex.type + ); + + return new ErrorDTO( + ErrorDTO.ErrorStage.DRAFT_ERROR, + ex.type.name() + ); + + } + + @MessageExceptionHandler(MatchException.class) + @SendToUser("/queue/errors") + public ErrorDTO handleMatchException(MatchException ex) { + log.warn( + "Match error: {}", + ex.type + ); + return new ErrorDTO( + ErrorDTO.ErrorStage.MATCH_ERROR, + ex.type.name() + ); + } +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/events/DraftSessionFinishedEvent.java b/src/main/java/com/codzilla/backend/PreMatch/events/DraftSessionFinishedEvent.java new file mode 100644 index 0000000..f627a7a --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/events/DraftSessionFinishedEvent.java @@ -0,0 +1,13 @@ +package com.codzilla.backend.PreMatch.events; + +import com.codzilla.backend.PreMatch.DraftSession.DraftSession; +import lombok.Getter; + +@Getter +public class DraftSessionFinishedEvent { + public DraftSessionFinishedEvent(DraftSession draftSession) { + this.draftSession = draftSession; + } + + DraftSession draftSession; +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/events/DraftSessionUpdatedEvent.java b/src/main/java/com/codzilla/backend/PreMatch/events/DraftSessionUpdatedEvent.java new file mode 100644 index 0000000..cf69aa0 --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/events/DraftSessionUpdatedEvent.java @@ -0,0 +1,13 @@ +package com.codzilla.backend.PreMatch.events; + +import com.codzilla.backend.PreMatch.DraftSession.DraftSession; +import lombok.Getter; + +@Getter +public class DraftSessionUpdatedEvent { + public DraftSessionUpdatedEvent(DraftSession draftSession) { + this.draftSession = draftSession; + } + + DraftSession draftSession; +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/exceptions/DraftSessionException.java b/src/main/java/com/codzilla/backend/PreMatch/exceptions/DraftSessionException.java new file mode 100644 index 0000000..6e66a5e --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/exceptions/DraftSessionException.java @@ -0,0 +1,14 @@ +package com.codzilla.backend.PreMatch.exceptions; + +public class DraftSessionException extends RuntimeException { + public enum DraftErrorType { + NOT_YOUR_TURN, + OPTION_DO_NOT_EXISTS, + CAN_NOT_BAN_LAST_OPTION, + OPTION_ALREADY_BANNED + } + public DraftErrorType type; + public DraftSessionException(DraftErrorType type) { + this.type = type; + } +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/exceptions/MatchException.java b/src/main/java/com/codzilla/backend/PreMatch/exceptions/MatchException.java new file mode 100644 index 0000000..4310982 --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/exceptions/MatchException.java @@ -0,0 +1,12 @@ +package com.codzilla.backend.PreMatch.exceptions; + + +public class MatchException extends RuntimeException { + public enum ErrorType { + + } + public ErrorType type; + public MatchException(ErrorType type) { + this.type = type; + } +} diff --git a/src/main/java/com/codzilla/backend/PreMatch/model/DraftSessionException.java b/src/main/java/com/codzilla/backend/PreMatch/model/DraftSessionException.java deleted file mode 100644 index 6115ec0..0000000 --- a/src/main/java/com/codzilla/backend/PreMatch/model/DraftSessionException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.codzilla.backend.PreMatch.model; - -public class DraftSessionException extends RuntimeException { - public DraftSessionException(String message) { - super(message); - } -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index aed14ef..e8cc45d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -10,7 +10,8 @@ app.s3.secret-key=${S3_PASSWORD} app.s3.region=${S3_REGION} app.s3.bucket-name=${S3_BUCKET_NAME} -app.draft-session.time-to-pick=10s +app.match.time-to-pick=20s +app.match.websocket-match-prefix=/topic/match/ spring.datasource.url=${DB_URL} spring.datasource.username=${DB_USERNAME} diff --git a/src/test/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionServiceTest.java b/src/test/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionServiceTest.java deleted file mode 100644 index bf14778..0000000 --- a/src/test/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionServiceTest.java +++ /dev/null @@ -1,543 +0,0 @@ -package com.codzilla.backend.PreMatch.DraftSession; - -import com.codzilla.backend.Authentication.dto.RegisterRequestDTO; -import com.codzilla.backend.BaseIntegrationTest; -import com.codzilla.backend.PreMatch.model.*; -import com.codzilla.backend.User.User; -import com.codzilla.backend.User.UserRepository; -import com.codzilla.backend.User.UserService; -import org.jspecify.annotations.Nullable; -import org.junit.jupiter.api.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.http.MediaType; -import org.springframework.messaging.converter.JacksonJsonMessageConverter; -import org.springframework.messaging.simp.stomp.StompHeaders; -import org.springframework.messaging.simp.stomp.StompSession; -import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; -import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.web.servlet.client.RestTestClient; -import org.springframework.web.servlet.support.WebContentGenerator; -import org.springframework.web.socket.WebSocketHttpHeaders; -import org.springframework.web.socket.client.standard.StandardWebSocketClient; -import org.springframework.web.socket.messaging.WebSocketStompClient; -import org.springframework.web.socket.sockjs.client.SockJsClient; -import org.springframework.web.socket.sockjs.client.Transport; -import org.springframework.web.socket.sockjs.client.WebSocketTransport; -import org.testcontainers.shaded.org.checkerframework.checker.units.qual.A; - -import java.lang.reflect.Type; -import java.time.Duration; -import java.util.*; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.when; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class DraftSessionServiceTest extends BaseIntegrationTest { - - private static final Logger log = LoggerFactory.getLogger(DraftSessionServiceTest.class); - @LocalServerPort - private int port; - - @MockitoBean - DraftSessionSettings draftSessionSettings; - - private RestTestClient restTestClient; - - private BlockingQueue user1Queue; - private BlockingQueue user2Queue; - - private StompSession user1Session; - private StompSession user2Session; - - @Autowired - private UserRepository userRepository; - - @Autowired - private DraftSessionRepository draftSessionRepository; - - @Autowired - private UserService userService; - - private User user1; - private User user2; - - String user1Jwt; - String user2Jwt; - - private DraftSession draftSession; - @Autowired - private WebContentGenerator webContentGenerator; - @Autowired - private DraftSessionService draftSessionService; - - @BeforeEach - void setup() throws Exception { - restTestClient = RestTestClient.bindToServer().baseUrl("http://localhost:" + port).build(); - - - userService.registerUser(new RegisterRequestDTO( - "user1", - "email1", - "password1" - )); - userService.registerUser(new RegisterRequestDTO( - "user2", - "email2", - "password2" - )); - - this.user1 = userService.getByEmail("email1"); - this.user2 = userService.getByEmail("email2"); - - this.user1Jwt = - restTestClient.post().uri("/auth/login").contentType(MediaType.APPLICATION_JSON) - .body(Map.of( - "email", - user1.getEmail(), - "rawPassword", - "password1" - )).exchange() - .expectStatus().isOk() - .returnResult().getResponseCookies().getFirst("jwt").getValue(); - - this.user2Jwt = - restTestClient.post().uri("/auth/login").contentType(MediaType.APPLICATION_JSON) - .body(Map.of( - "email", - user2.getEmail(), - "rawPassword", - "password2" - )).exchange() - .expectStatus().isOk() - .returnResult().getResponseCookies().getFirst("jwt").getValue(); - when(draftSessionSettings.getTimeToPick()).thenReturn(Duration.ofMillis(50000)); - createDraftSession(); - - List transports = List.of(new WebSocketTransport(new StandardWebSocketClient())); - - SockJsClient sockJsClient1 = new SockJsClient(transports); - SockJsClient sockJsClient2 = new SockJsClient(transports); - - var stompClient1 = new WebSocketStompClient(sockJsClient1); - var stompClient2 = new WebSocketStompClient(sockJsClient2); - - stompClient1.setMessageConverter(new JacksonJsonMessageConverter()); - stompClient2.setMessageConverter(new JacksonJsonMessageConverter()); - - WebSocketHttpHeaders httpHeaders1 = new WebSocketHttpHeaders(); - WebSocketHttpHeaders httpHeaders2 = new WebSocketHttpHeaders(); - - httpHeaders1.add( - "Cookie", - "jwt=" + user1Jwt - ); - httpHeaders2.add( - "Cookie", - "jwt=" + user2Jwt - ); - - String url = "ws://localhost:" + port + "/game-ws"; - CompletableFuture completableFuture1 = stompClient1.connectAsync( - url, - httpHeaders1, - new StompHeaders(), - new StompSessionHandlerAdapter() { - } - ); - CompletableFuture completableFuture2 = stompClient2.connectAsync( - url, - httpHeaders2, - new StompHeaders(), - new StompSessionHandlerAdapter() { - } - ); - user1Session = completableFuture1.get( - 5, - TimeUnit.SECONDS - ); - user2Session = completableFuture2.get( - 5, - TimeUnit.SECONDS - ); - - user1Queue = new LinkedBlockingQueue<>(); - user2Queue = new LinkedBlockingQueue<>(); - - user1Session.subscribe( - "/topic/draft-session/" + draftSession.getId(), - new StompSessionHandlerAdapter() { - @Override - public Type getPayloadType(StompHeaders headers) { - return DraftSessionResponseDTO.class; - } - - @Override - public void handleFrame(StompHeaders headers, @Nullable Object payload) { - user1Queue.add((DraftSessionResponseDTO) payload); - } - } - ); - user1Session.subscribe( - "/user/queue/errors", - new StompSessionHandlerAdapter() { - @Override - public Type getPayloadType(StompHeaders headers) { - return DraftSessionResponseDTO.class; - } - - @Override - public void handleFrame(StompHeaders headers, @Nullable Object payload) { - user1Queue.add((DraftSessionResponseDTO) payload); - } - } - ); - user2Session.subscribe( - "/topic/draft-session/" + draftSession.getId(), - new StompSessionHandlerAdapter() { - @Override - public Type getPayloadType(StompHeaders headers) { - return DraftSessionResponseDTO.class; - } - - @Override - public void handleFrame(StompHeaders headers, @Nullable Object payload) { - user2Queue.add((DraftSessionResponseDTO) payload); - } - } - ); - user2Session.subscribe( - "/user/queue/errors", - new StompSessionHandlerAdapter() { - @Override - public Type getPayloadType(StompHeaders headers) { - return DraftSessionResponseDTO.class; - } - - @Override - public void handleFrame(StompHeaders headers, @Nullable Object payload) { - user2Queue.add((DraftSessionResponseDTO) payload); - } - } - ); - user1Queue.take(); - user2Queue.take(); - - } - - @AfterEach - void tearDown() { - draftSessionRepository.deleteAll(); - userRepository.deleteAll(); - - if (user1Session != null) { - user1Session.disconnect(); - } - - if (user2Session != null) { - user2Session.disconnect(); - } - } - - void createDraftSession() { - this.draftSession = draftSessionService.startDraftSession( - user1.getId(), - user2.getId() - ); - log.info("Draft session id: " + draftSession.getId()); - } - - @Test - void DraftSession_HappyPath() throws InterruptedException { - user1Session.send( - "/app/" + draftSession.getId() + "/ban", - new OptionEntity( - Category.Language, - Language.CPP.name() - ) - ); - - - var res1 = user1Queue.poll(1, TimeUnit.SECONDS); - var res2 = user2Queue.poll(1, TimeUnit.SECONDS); - - assertNotEquals( - null, - res1 - ); - assertNotEquals( - null, - res2 - ); - - assertEquals( - DraftSessionResponseDTO.Status.SUCCEED, - res1.getStatus() - ); - - assertFalse(res1.getDraftSession().getRemainOptions().get(Category.Language) - .contains(Language.CPP.name())); - - assertFalse(res1.getDraftSession().isFirstUserMove); - - user2Session.send( - "/app/" + draftSession.getId() + "/ban", - new OptionEntity( - Category.ProblemType, - ProblemType.Algorithm.name() - ) - ); - - res1 = user1Queue.take(); - - res2 = user2Queue.take(); - - assertNotEquals( - null, - res1 - ); - assertNotEquals( - null, - res2 - ); - - assertFalse(res2.getDraftSession().remainOptions.get(Category.ProblemType) - .contains(ProblemType.Algorithm.name())); - } - - @Test - void DraftSession_OneUserCantBanTwice() throws InterruptedException { - user1Session.send( - "/app/" + draftSession.getId() + "/ban", - new OptionEntity( - Category.Language, - Language.CPP.name() - ) - ); - - var res = user1Queue.take(); - assertNotNull(res); - - assertEquals( - DraftSessionResponseDTO.Status.SUCCEED, - res.getStatus() - ); - assertFalse(res.getDraftSession().getRemainOptions().get(Category.Language) - .contains(Language.CPP.name())); - log.info( - "FIRST: {}", - res.getDraftSession().getRemainOptions() - ); - - user1Session.send( - "/app/" + draftSession.getId() + "/ban", - new OptionEntity( - Category.ProblemType, - ProblemType.Algorithm.name() - ) - ); - - res = user1Queue.poll( - 1, - TimeUnit.SECONDS - ); - assertNotNull(res); - assertEquals( - DraftSessionResponseDTO.Status.ERROR, - res.getStatus() - ); - - } - - @Test - void DraftSession_CantBanWrongOption() throws InterruptedException { - user1Session.send( - "/app/" + draftSession.getId() + "/ban", - new OptionEntity( - Category.Language, - "wrong language" - ) - ); - - var res = user1Queue.poll( - 1, - TimeUnit.SECONDS - ); - assertNotNull(res); - assertEquals( - DraftSessionResponseDTO.Status.ERROR, - res.getStatus(), - "We cant ban not existing option!" - ); - - user1Session.send( - "/app/" + draftSession.getId() + "/ban", - new OptionEntity( - Category.ProblemType, - ProblemType.Algorithm.name() - ) - ); - - res = user1Queue.poll( - 1, - TimeUnit.SECONDS - ); - user2Queue.poll( - 1, - TimeUnit.SECONDS - ); - assertNotNull(res); - assertEquals( - DraftSessionResponseDTO.Status.SUCCEED, - res.getStatus() - ); - user2Session.send( - "/app/" + draftSession.getId() + "/ban", - new OptionEntity( - Category.ProblemType, - ProblemType.Algorithm.name() - ) - ); - - res = user2Queue.poll( - 1, - TimeUnit.SECONDS - ); - - assertNotNull(res); - - assertEquals( - DraftSessionResponseDTO.Status.ERROR, - res.getStatus(), - "We cant ban one option twice!" - ); - - } - - @Test - void DraftSession_CantBanTheLastOptionInCategory() throws InterruptedException { - - var categoryOptional = Arrays.stream(Category.values()).findFirst(); - assertTrue( - categoryOptional.isPresent(), - "There is no category at all!" - ); - var category = categoryOptional.get(); - - List user1Messages = new ArrayList<>(); - List user2Messages = new ArrayList<>(); - for (var option : category.getEnumClass().getEnumConstants()) { - (draftSession.isFirstUserMove() ? user1Session : user2Session) - .send( - "/app/" + draftSession.getId() + "/ban", - new OptionEntity( - category, - option.name() - ) - ); - - log.info( - "{}, {}", - category, - option - ); - var res1 = user1Queue.poll( - 1, - TimeUnit.SECONDS - ); - var res2 = user2Queue.poll( - 1, - TimeUnit.SECONDS - ); - if (res1 != null) { - user1Messages.add(res1); - } - if (res2 != null) { - user2Messages.add(res2); - } - - draftSession.setFirstUserMove(!draftSession.isFirstUserMove); - } - - assertTrue( - user1Messages.getLast().getStatus() == DraftSessionResponseDTO.Status.ERROR || - user2Messages.getLast().getStatus() == DraftSessionResponseDTO.Status.ERROR - ); - } - - @Test - void DraftSession_WhenTimeToPickExceededItIsRandomBan() throws InterruptedException { - when(draftSessionSettings.getTimeToPick()).thenReturn(Duration.ofMillis(100)); - user1Session.send( - "/app/" + draftSession.getId() + "/ban", - new OptionEntity( - Category.Language, - Language.CPP.name() - ) - ); - - var res = user1Queue.take(); - assertFalse(res.getDraftSession().isFirstUserMove()); - var optionsRemainAfterFirstBan = res.getDraftSession().getRemainOptions().values() - .stream().map(Set::size) - .reduce( - 0, - Integer::sum - ); - - var resAfterRandomBan = user1Queue.poll( - 110, - TimeUnit.MILLISECONDS - ); - - assertNotNull(resAfterRandomBan); - assertNotNull(resAfterRandomBan.getDraftSession()); - assertTrue(resAfterRandomBan.getDraftSession().isFirstUserMove()); - - var optionsRemainAfterRandomBan = - resAfterRandomBan.getDraftSession().getRemainOptions().values() - .stream().map(Set::size) - .reduce( - 0, - Integer::sum - ); - - assertEquals( - optionsRemainAfterFirstBan - 1, - optionsRemainAfterRandomBan - ); - } - -// @Test -// void fullAutoPickSession() throws InterruptedException { -// when(draftSessionSettings.getTimeToPick()).thenReturn(Duration.ofMillis(100)); -// Thread.sleep(5000); -// DraftSessionResponseDTO last = null; -// while (!user1Queue.isEmpty()) { -// last = user1Queue.take(); -// } -// assertNotNull(last); -// assertEquals( -// DraftSessionResponseDTO.Status.SUCCEED, -// last.getStatus() -// ); -// assertEquals( -// DraftSession.Status.PICKING, -// last.getDraftSession().getStatus() -// ); -// -// assertTrue(last.getDraftSession().getRemainOptions().values().stream() -// .allMatch((options) -> options.size() == 1)); -// } - -} \ No newline at end of file