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/DTO/DraftSessionResponseDTO.java b/src/main/java/com/codzilla/backend/PreMatch/DTO/DraftSessionResponseDTO.java new file mode 100644 index 0000000..be74d60 --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/DTO/DraftSessionResponseDTO.java @@ -0,0 +1,14 @@ +package com.codzilla.backend.PreMatch.DTO; + +import com.codzilla.backend.PreMatch.DraftSession.DraftSession; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + + +@AllArgsConstructor +@Getter +@Setter +public class DraftSessionResponseDTO { + DraftSession draftSession; +} 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 new file mode 100644 index 0000000..69f8199 --- /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 + @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 matchId, UUID firstUserId, UUID secondUserId) { + this(); + this.firstUserId = firstUserId; + this.secondUserId = secondUserId; + this.id = matchId; + } + + public enum Status { + PICKING, + FINISHED + } +} 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/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..e305c16 --- /dev/null +++ b/src/main/java/com/codzilla/backend/PreMatch/DraftSession/DraftSessionService.java @@ -0,0 +1,209 @@ +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; +import org.springframework.transaction.support.TransactionTemplate; + +import java.time.Instant; +import java.util.*; +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, + TransactionTemplate transactionTemplate, + MatchSettings matchSettings, + ApplicationEventPublisher eventPublisher + ) { + this.draftSessionRepository = draftSessionRepository; + this.taskScheduler = taskScheduler; + this.transactionTemplate = transactionTemplate; + this.matchSettings = matchSettings; + this.eventPublisher = eventPublisher; + } + + + private final DraftSessionRepository draftSessionRepository; + private final TaskScheduler taskScheduler; + private final TransactionTemplate transactionTemplate; + private final MatchSettings matchSettings; + private final ApplicationEventPublisher eventPublisher; + 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)); + + var nowUserMoving = + draftSession.isFirstUserMove ? draftSession.getFirstUserId() : + draftSession.getSecondUserId(); + + if (!nowUserMoving.equals(user.getId())) { + throw new DraftSessionException(DraftSessionException.DraftErrorType.NOT_YOUR_TURN); + } + + banOption( + draftSession, + optionEntity + ); + + if (draftSession.getStatus().equals(DraftSession.Status.PICKING)) { + startTimer(draftSessionId); + draftSessionRepository.save(draftSession); + } else if (draftSession.getStatus().equals(DraftSession.Status.FINISHED)) { + draftSessionRepository.deleteById(draftSessionId); + } + + 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(DraftSessionException.DraftErrorType.OPTION_DO_NOT_EXISTS); + } + + long optionsOfCategoryRemain = + draftSession.remainOptions.get(optionEntity.getCategory()).size(); + + if (optionsOfCategoryRemain == 1) { + throw new DraftSessionException(DraftSessionException.DraftErrorType.CAN_NOT_BAN_LAST_OPTION); + } + + if (!draftSession.remainOptions.get(optionEntity.getCategory()) + .contains(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 = + draftSession.remainOptions.values().stream().allMatch( + set -> set.size() == 1 + ); + + if (isDraftSessionFinished) { + draftSession.setStatus(DraftSession.Status.FINISHED); + eventPublisher.publishEvent(new DraftSessionFinishedEvent(draftSession)); + } else { + eventPublisher.publishEvent(new DraftSessionUpdatedEvent(draftSession)); + } + } + + 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(matchSettings.getTimeToPick()) + ); + scheduledTasks.put( + draftSessionId, + task + ); + } + + private void cancelTimer(UUID draftSessionId) { + ScheduledFuture task = scheduledTasks.remove(draftSessionId); + if (task != null) { + task.cancel(false); + } + } + + 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) + ); + break; + } catch (RuntimeException _) { + } + } + + + draftSessionRepository.save(draftSession); + if (draftSession.getStatus().equals(DraftSession.Status.PICKING)) { + startTimer(draftSessionId); + } + } + + public Optional findById(UUID id) { + return draftSessionRepository.findById(id); + } + + 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/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/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/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/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..2c98baa --- /dev/null +++ b/src/main/java/com/codzilla/backend/WebSocket/WebSocketConfig.java @@ -0,0 +1,49 @@ +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", "/queue"); + config.setApplicationDestinationPrefixes("/app", "/topic"); + config.setUserDestinationPrefix("/user"); + } + + @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..e8cc45d 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,9 @@ app.s3.secret-key=${S3_PASSWORD} app.s3.region=${S3_REGION} app.s3.bucket-name=${S3_BUCKET_NAME} +app.match.time-to-pick=20s +app.match.websocket-match-prefix=/topic/match/ + spring.datasource.url=${DB_URL} spring.datasource.username=${DB_USERNAME} spring.datasource.password=${DB_PASSWORD} @@ -28,3 +30,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/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