diff --git a/backend-submodule b/backend-submodule index 79b9668a..7ebcff22 160000 --- a/backend-submodule +++ b/backend-submodule @@ -1 +1 @@ -Subproject commit 79b9668a54dc4a65541f7822dc2a3e72496104fd +Subproject commit 7ebcff2298759892c7a1348841fce473feb761ea diff --git a/src/main/java/mafia/mafiatogether/MafiaTogetherApplication.java b/src/main/java/mafia/mafiatogether/MafiaTogetherApplication.java index 7003b4e0..48aa2921 100644 --- a/src/main/java/mafia/mafiatogether/MafiaTogetherApplication.java +++ b/src/main/java/mafia/mafiatogether/MafiaTogetherApplication.java @@ -2,14 +2,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication -@EnableScheduling public class MafiaTogetherApplication { public static void main(String[] args) { SpringApplication.run(MafiaTogetherApplication.class, args); } - } diff --git a/src/main/java/mafia/mafiatogether/common/config/RedisConfig.java b/src/main/java/mafia/mafiatogether/common/config/RedisConfig.java index 7f0cfcfc..1a911283 100644 --- a/src/main/java/mafia/mafiatogether/common/config/RedisConfig.java +++ b/src/main/java/mafia/mafiatogether/common/config/RedisConfig.java @@ -17,10 +17,10 @@ @EnableRedisRepositories public class RedisConfig { - @Value("${spring.redis.host}") + @Value("${spring.data.redis.host}") private String host; - @Value(("${spring.redis.port}")) + @Value("${spring.data.redis.port}") private int port; @Bean @@ -63,5 +63,4 @@ public RedisMessageListenerContainer redisMessageListenerContainer( return container; } - } diff --git a/src/main/java/mafia/mafiatogether/common/config/SchedulerConfig.java b/src/main/java/mafia/mafiatogether/common/config/SchedulerConfig.java new file mode 100644 index 00000000..e1013a3e --- /dev/null +++ b/src/main/java/mafia/mafiatogether/common/config/SchedulerConfig.java @@ -0,0 +1,11 @@ +package mafia.mafiatogether.common.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@Configuration +@EnableScheduling +@ConditionalOnProperty(value = "application.scheduling-enable", havingValue = "true", matchIfMissing = false) +public class SchedulerConfig { +} diff --git a/src/main/java/mafia/mafiatogether/common/config/WebMvcConfig.java b/src/main/java/mafia/mafiatogether/common/config/WebMvcConfig.java index e0ed2ba3..988dd515 100644 --- a/src/main/java/mafia/mafiatogether/common/config/WebMvcConfig.java +++ b/src/main/java/mafia/mafiatogether/common/config/WebMvcConfig.java @@ -1,7 +1,5 @@ package mafia.mafiatogether.common.config; -import java.util.List; - import mafia.mafiatogether.common.resolver.PlayerArgumentResolver; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; @@ -9,6 +7,8 @@ import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.List; + @Configuration public class WebMvcConfig implements WebMvcConfigurer { @@ -20,7 +20,12 @@ public void addArgumentResolvers(final List resol @Override public void addCorsMappings(final CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("https://mafia-together.com", "http://localhost:5173", "https://localhost:5173") + .allowedOrigins( + "https://dev.mafia-together.com", + "https://mafia-together.com", + "http://localhost:5173", + "https://localhost:5173" + ) .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") .allowCredentials(true) .exposedHeaders(HttpHeaders.LOCATION); diff --git a/src/main/java/mafia/mafiatogether/common/config/WebSocketConfig.java b/src/main/java/mafia/mafiatogether/common/config/WebSocketConfig.java index d332b05e..b5e20e9c 100644 --- a/src/main/java/mafia/mafiatogether/common/config/WebSocketConfig.java +++ b/src/main/java/mafia/mafiatogether/common/config/WebSocketConfig.java @@ -19,7 +19,13 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/stomp"); + registry.addEndpoint("/stomp") + .setAllowedOrigins( + "https://dev.mafia-together.com", + "https://mafia-together.com", + "http://localhost:5173", + "https://localhost:5173" + ); } @Override diff --git a/src/main/java/mafia/mafiatogether/game/annotation/SseSubscribe.java b/src/main/java/mafia/mafiatogether/game/annotation/SseSubscribe.java new file mode 100644 index 00000000..d7eca873 --- /dev/null +++ b/src/main/java/mafia/mafiatogether/game/annotation/SseSubscribe.java @@ -0,0 +1,7 @@ +package mafia.mafiatogether.game.annotation; +import java.lang.annotation.*; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SseSubscribe { +} diff --git a/src/main/java/mafia/mafiatogether/game/application/GameEventListener.java b/src/main/java/mafia/mafiatogether/game/application/GameEventListener.java index 329df7ae..67c47be8 100644 --- a/src/main/java/mafia/mafiatogether/game/application/GameEventListener.java +++ b/src/main/java/mafia/mafiatogether/game/application/GameEventListener.java @@ -1,22 +1,11 @@ package mafia.mafiatogether.game.application; -import java.io.IOException; -import java.time.Clock; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; import lombok.RequiredArgsConstructor; import mafia.mafiatogether.chat.domain.Chat; import mafia.mafiatogether.chat.domain.ChatRepository; import mafia.mafiatogether.common.exception.ExceptionCode; import mafia.mafiatogether.common.exception.GameException; -import mafia.mafiatogether.game.application.dto.event.ClearJobTargetEvent; -import mafia.mafiatogether.game.application.dto.event.ClearVoteEvent; -import mafia.mafiatogether.game.application.dto.event.DeleteGameEvent; -import mafia.mafiatogether.game.application.dto.event.GameStatusChangeEvent; -import mafia.mafiatogether.game.application.dto.event.JobExecuteEvent; -import mafia.mafiatogether.game.application.dto.event.StartGameEvent; -import mafia.mafiatogether.game.application.dto.event.VoteExecuteEvent; +import mafia.mafiatogether.game.application.dto.event.*; import mafia.mafiatogether.game.application.dto.response.GameStatusResponse; import mafia.mafiatogether.game.domain.Game; import mafia.mafiatogether.game.domain.GameRepository; @@ -38,6 +27,12 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter.SseEventBuilder; +import java.io.IOException; +import java.time.Clock; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + @Component @RequiredArgsConstructor public class GameEventListener { @@ -109,6 +104,7 @@ public void listenDeleteGameEvent(final DeleteGameEvent deleteGameEvent) { jobTargetRepository.deleteById(deleteGameEvent.code()); chatRepository.deleteById(deleteGameEvent.code()); voteRepository.deleteById(deleteGameEvent.code()); + sseEmitterRepository.deleteByCode(deleteGameEvent.code()); gameRepository.deleteById(deleteGameEvent.code()); final Lobby room = lobbyRepository.findById(deleteGameEvent.code()) @@ -138,6 +134,7 @@ public void listenDeleteLobbyEvent(final DeleteLobbyEvent deleteLobbyEvent) { jobTargetRepository.deleteById(deleteLobbyEvent.code()); chatRepository.deleteById(deleteLobbyEvent.code()); voteRepository.deleteById(deleteLobbyEvent.code()); + sseEmitterRepository.deleteByCode(deleteLobbyEvent.code()); gameRepository.deleteById(deleteLobbyEvent.code()); lobbyRepository.deleteById(deleteLobbyEvent.code()); } @@ -148,7 +145,7 @@ public void listenGameStatusChangeEvent(final GameStatusChangeEvent gameStatusCh } private void sendStatusChangeEventToSseClient(final String code, final StatusType statusType) throws IOException { - List emitters = sseEmitterRepository.get(code); + List emitters = sseEmitterRepository.findByCode(code); for (SseEmitter emitter : emitters) { emitter.send(getSseEvent(statusType)); } diff --git a/src/main/java/mafia/mafiatogether/game/application/GameService.java b/src/main/java/mafia/mafiatogether/game/application/GameService.java index d5025ffb..8e33f3f7 100644 --- a/src/main/java/mafia/mafiatogether/game/application/GameService.java +++ b/src/main/java/mafia/mafiatogether/game/application/GameService.java @@ -1,8 +1,5 @@ package mafia.mafiatogether.game.application; -import java.io.IOException; -import java.time.Clock; -import java.util.Optional; import lombok.RequiredArgsConstructor; import mafia.mafiatogether.common.exception.ExceptionCode; import mafia.mafiatogether.common.exception.GameException; @@ -11,25 +8,22 @@ import mafia.mafiatogether.game.application.dto.response.GameStatusResponse; import mafia.mafiatogether.game.domain.Game; import mafia.mafiatogether.game.domain.GameRepository; -import mafia.mafiatogether.game.domain.SseEmitterRepository; import mafia.mafiatogether.game.domain.status.StatusType; import mafia.mafiatogether.lobby.domain.Lobby; import mafia.mafiatogether.lobby.domain.LobbyRepository; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter.SseEventBuilder; + +import java.time.Clock; +import java.util.Optional; @Service @RequiredArgsConstructor public class GameService { - private static final String SSE_STATUS = "gameStatus"; - private static final String SSE_CONNECT_DATA = "connect"; private final LobbyRepository lobbyRepository; private final GameRepository gameRepository; - private final SseEmitterRepository sseEmitterRepository; @Transactional public GameStatusResponse findStatus(final String code) { @@ -46,7 +40,7 @@ public GameStatusResponse findStatus(final String code) { private StatusType checkStatusChanged(final Game game) { game.setStatsSnapshot(); final StatusType statusType = game.getStatusType(Clock.systemDefaultZone().millis()); - if (game.isDeleted()){ + if (game.isDeleted()) { gameRepository.delete(game); return StatusType.WAIT; } @@ -90,31 +84,9 @@ public GameResultResponse findResult(final String code) { return GameResultResponse.from(game); } - @Transactional - public SseEmitter subscribe(final String code) throws IOException { - SseEmitter sseEmitter = new SseEmitter(43200_000L); - sseEmitter.send(getSseEvent(code)); - sseEmitterRepository.save(code, sseEmitter); - return sseEmitter; - } - - private SseEventBuilder getSseEvent(final String code) { - Optional game = gameRepository.findById(code); - if (game.isPresent()) { - return SseEmitter.event() - .name(SSE_STATUS) - .data(new GameStatusResponse(game.get().getStatus().getType())) - .reconnectTime(30_000L); - } - return SseEmitter.event() - .name(SSE_STATUS) - .data(new GameStatusResponse(StatusType.WAIT)) - .reconnectTime(30_000L); - } - @Scheduled(fixedDelay = 500L) @Transactional - public void changeStatus(){ + public void changeStatus() { for (Game game : gameRepository.findAll()) { checkStatusChanged(game); } diff --git a/src/main/java/mafia/mafiatogether/game/aspect/SseService.java b/src/main/java/mafia/mafiatogether/game/aspect/SseService.java new file mode 100644 index 00000000..e86b9c28 --- /dev/null +++ b/src/main/java/mafia/mafiatogether/game/aspect/SseService.java @@ -0,0 +1,87 @@ +package mafia.mafiatogether.game.aspect; + +import lombok.RequiredArgsConstructor; +import mafia.mafiatogether.common.annotation.PlayerInfo; +import mafia.mafiatogether.common.exception.AuthException; +import mafia.mafiatogether.common.exception.ExceptionCode; +import mafia.mafiatogether.game.application.dto.response.GameStatusResponse; +import mafia.mafiatogether.game.domain.Game; +import mafia.mafiatogether.game.domain.GameRepository; +import mafia.mafiatogether.game.domain.SseEmitterRepository; +import mafia.mafiatogether.game.domain.status.StatusType; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Optional; + +@Aspect +@Component +@RequiredArgsConstructor +public class SseService { + + private static final String SSE_STATUS = "gameStatus"; + public static final long HOURS_12 = 43200_000L; + public static final long SECOND_30 = 30_000L; + private final SseEmitterRepository sseEmitterRepository; + private final GameRepository gameRepository; + + @Around("@annotation(mafia.mafiatogether.game.annotation.SseSubscribe)") + public ResponseEntity subscribe(final ProceedingJoinPoint joinPoint) throws Throwable { + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + Method method = methodSignature.getMethod(); + + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + Object[] args = joinPoint.getArgs(); + + String[] codeAndName = new String[2]; + for (int i = 0; i < parameterAnnotations.length; i++) { + if (hasPlayerInfo(parameterAnnotations[i])) { + codeAndName[0] = args[i].toString(); + codeAndName[1] = args[i].toString(); + break; + } + } + + if (codeAndName[0] == null || codeAndName[1] == null) { + throw new AuthException(ExceptionCode.INVALID_AUTHENTICATION_FORM); + } + + SseEmitter sseEmitter = createSseEmitter(codeAndName[0], codeAndName[1]); + sseEmitterRepository.save(codeAndName[0], codeAndName[1], sseEmitter); + return ResponseEntity.ok(sseEmitter); + } + + private boolean hasPlayerInfo(Annotation[] annotations) { + return Arrays.stream(annotations).anyMatch(PlayerInfo.class::isInstance); + } + + private SseEmitter createSseEmitter(String code, String name) throws IOException { + SseEmitter sseEmitter = new SseEmitter(HOURS_12); + sseEmitter.send(getSseEvent(code)); + sseEmitter.onCompletion(() -> sseEmitterRepository.deleteByCodeAndEmitter(code, name)); + sseEmitter.onTimeout(sseEmitter::complete); + return sseEmitter; + } + + private SseEmitter.SseEventBuilder getSseEvent(final String code) { + Optional game = gameRepository.findById(code); + return game.map(value -> getSseEventBuilder(value.getStatus().getType())) + .orElseGet(() -> getSseEventBuilder(StatusType.WAIT)); + } + + private static SseEmitter.SseEventBuilder getSseEventBuilder(final StatusType statusType) { + return SseEmitter.event() + .name(SSE_STATUS) + .data(new GameStatusResponse(statusType)) + .reconnectTime(SECOND_30); + } +} diff --git a/src/main/java/mafia/mafiatogether/game/domain/InMemorySseEmitterRepository.java b/src/main/java/mafia/mafiatogether/game/domain/InMemorySseEmitterRepository.java index e5182c63..ab578b7a 100644 --- a/src/main/java/mafia/mafiatogether/game/domain/InMemorySseEmitterRepository.java +++ b/src/main/java/mafia/mafiatogether/game/domain/InMemorySseEmitterRepository.java @@ -4,28 +4,45 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; + import org.springframework.stereotype.Repository; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @Repository public class InMemorySseEmitterRepository implements SseEmitterRepository { - private final Map> emitters; + private final Map> emitters; public InMemorySseEmitterRepository() { this.emitters = new ConcurrentHashMap<>(); } @Override - public void save(final String code, final SseEmitter sseEmitter) { + public void save(final String code, final String name, final SseEmitter sseEmitter) { + if (!emitters.containsKey(code)) { + emitters.put(code, new ConcurrentHashMap<>()); + } + emitters.get(code).put(name, sseEmitter); + } + + @Override + public List findByCode(String code) { if (!emitters.containsKey(code)) { - emitters.put(code, new ArrayList<>()); + return new ArrayList<>(); } - emitters.get(code).add(sseEmitter); + return emitters.get(code).values().stream().toList(); } @Override - public List get(String code) { - return emitters.get(code); + public void deleteByCode(String code) { + emitters.remove(code); + } + + @Override + public void deleteByCodeAndEmitter(String code, final String name) { + if (!emitters.containsKey(code)) { + return; + } + emitters.get(code).remove(name); } } diff --git a/src/main/java/mafia/mafiatogether/game/domain/SseEmitterRepository.java b/src/main/java/mafia/mafiatogether/game/domain/SseEmitterRepository.java index c4c9f351..34052959 100644 --- a/src/main/java/mafia/mafiatogether/game/domain/SseEmitterRepository.java +++ b/src/main/java/mafia/mafiatogether/game/domain/SseEmitterRepository.java @@ -1,12 +1,17 @@ package mafia.mafiatogether.game.domain; import java.util.List; + import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; public interface SseEmitterRepository { - void save(final String code, final SseEmitter sseEmitter); + void save(final String code, final String name, final SseEmitter sseEmitter); + + List findByCode(final String code); + + void deleteByCode(final String code); - List get(final String code); + void deleteByCodeAndEmitter(final String code, final String name); } diff --git a/src/main/java/mafia/mafiatogether/game/domain/status/StatusType.java b/src/main/java/mafia/mafiatogether/game/domain/status/StatusType.java index 2ab5315f..6dc16c4d 100644 --- a/src/main/java/mafia/mafiatogether/game/domain/status/StatusType.java +++ b/src/main/java/mafia/mafiatogether/game/domain/status/StatusType.java @@ -12,4 +12,5 @@ public enum StatusType { NIGHT, END, DELETED + } diff --git a/src/main/java/mafia/mafiatogether/game/ui/GameController.java b/src/main/java/mafia/mafiatogether/game/ui/GameController.java index 7ec9f957..aa6d3c53 100644 --- a/src/main/java/mafia/mafiatogether/game/ui/GameController.java +++ b/src/main/java/mafia/mafiatogether/game/ui/GameController.java @@ -1,10 +1,10 @@ package mafia.mafiatogether.game.ui; -import java.io.IOException; import lombok.RequiredArgsConstructor; import mafia.mafiatogether.common.annotation.PlayerInfo; -import mafia.mafiatogether.game.application.GameService; import mafia.mafiatogether.common.resolver.PlayerInfoDto; +import mafia.mafiatogether.game.annotation.SseSubscribe; +import mafia.mafiatogether.game.application.GameService; import mafia.mafiatogether.game.application.dto.response.GameExistResponse; import mafia.mafiatogether.game.application.dto.response.GameInfoResponse; import mafia.mafiatogether.game.application.dto.response.GameResultResponse; @@ -54,13 +54,11 @@ public ResponseEntity findGameInfo( } @GetMapping(path = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public ResponseEntity subscribe( - @PlayerInfo final PlayerInfoDto playerInfoDto - ) throws IOException { - return ResponseEntity.ok(gameService.subscribe(playerInfoDto.code())); + @SseSubscribe + public ResponseEntity subscribe(@PlayerInfo final PlayerInfoDto playerInfoDto) { + return ResponseEntity.ok(new SseEmitter()); } - @GetMapping("/valid") public ResponseEntity isValid( @PlayerInfo final PlayerInfoDto playerInfoDto diff --git a/src/test/java/mafia/mafiatogether/game/application/GameEventListenerTest.java b/src/test/java/mafia/mafiatogether/game/application/GameEventListenerTest.java index 909bbeff..7edcc372 100644 --- a/src/test/java/mafia/mafiatogether/game/application/GameEventListenerTest.java +++ b/src/test/java/mafia/mafiatogether/game/application/GameEventListenerTest.java @@ -240,7 +240,7 @@ void setTest() { final String target = PLAYER1_NAME; game.skipStatus(Clock.systemDefaultZone().millis()); // NOTICE game.skipStatus(Clock.systemDefaultZone().millis()); // DAY - Mockito.when(sseEmitterRepository.get(any())).thenReturn(List.of()); + Mockito.when(sseEmitterRepository.findByCode(any())).thenReturn(List.of()); gameRepository.save(game); // when @@ -254,6 +254,6 @@ void setTest() { // then final StatusType actual = gameRepository.findById(CODE).get().getStatus().getType(); Assertions.assertThat(actual).isEqualTo(StatusType.VOTE); - Mockito.verify(sseEmitterRepository, Mockito.atLeast(1)).get(CODE); + Mockito.verify(sseEmitterRepository, Mockito.atLeast(1)).findByCode(CODE); } } diff --git a/src/test/java/mafia/mafiatogether/game/application/GameServiceTest.java b/src/test/java/mafia/mafiatogether/game/application/GameServiceTest.java index 93adcf34..2fce8a04 100644 --- a/src/test/java/mafia/mafiatogether/game/application/GameServiceTest.java +++ b/src/test/java/mafia/mafiatogether/game/application/GameServiceTest.java @@ -1,22 +1,12 @@ package mafia.mafiatogether.game.application; -import static org.assertj.core.api.SoftAssertions.assertSoftly; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; - -import io.restassured.RestAssured; -import java.io.IOException; -import java.time.Clock; -import java.time.Duration; -import java.util.List; -import java.util.Optional; import mafia.mafiatogether.game.domain.Game; import mafia.mafiatogether.game.domain.GameRepository; import mafia.mafiatogether.game.domain.PlayerCollection; import mafia.mafiatogether.game.domain.SseEmitterRepository; import mafia.mafiatogether.game.domain.status.DayIntroStatus; import mafia.mafiatogether.game.domain.status.StatusType; -import mafia.mafiatogether.global.RedisTestConfig; +import mafia.mafiatogether.global.RedisTestContainerSpringBootTest; import mafia.mafiatogether.job.domain.JobTarget; import mafia.mafiatogether.job.domain.JobTargetRepository; import mafia.mafiatogether.lobby.domain.Lobby; @@ -26,19 +16,18 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.context.annotation.Import; -@Import(RedisTestConfig.class) -@SuppressWarnings("NonAsciiCharacters") -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -public class GameServiceTest { +import java.io.IOException; +import java.time.Clock; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.mockito.ArgumentMatchers.any; - @LocalServerPort - private int port; +@SuppressWarnings("NonAsciiCharacters") +public class GameServiceTest extends RedisTestContainerSpringBootTest { @Autowired protected LobbyRepository lobbyRepository; @@ -59,11 +48,6 @@ public class GameServiceTest { private static Game NOTCHANGEDGAME; private static Long now; - @BeforeEach - void setPort() { - RestAssured.port = port; - } - @BeforeEach void setGames() { now = Clock.systemDefaultZone().millis(); @@ -93,15 +77,11 @@ void setGames() { void 스케쥴러에_의해_방의_시간이_변경된다() { // given JobTarget mockedJobTarget = Mockito.mock(JobTarget.class); - Mockito.when(sseEmitterRepository.get(any())).thenReturn(List.of()); + Mockito.when(sseEmitterRepository.findByCode(any())).thenReturn(List.of()); Mockito.when(jobTargetRepository.findById(any())).thenReturn(Optional.of(mockedJobTarget)); // when & then - await().atMost(Duration.ofMillis(4_000L)) - .untilAsserted(() -> assertStatusType()); - } - - private void assertStatusType() { + gameService.changeStatus(); final StatusType actualChanged = gameRepository.findById(STATUSCHANGEDGAME.getCode()).get().getStatus() .getType(); final StatusType actualNotChanged = gameRepository.findById(NOTCHANGEDGAME.getCode()).get().getStatus() @@ -115,18 +95,18 @@ private void assertStatusType() { ); } -// @Test -// void 상태가_변경시_이벤트가_발행된다() throws IOException { -// // given -// JobTarget mockedJobTarget = Mockito.mock(JobTarget.class); -// Mockito.when(sseEmitterRepository.get(any())).thenReturn(List.of()); -// Mockito.when(jobTargetRepository.findById(any())).thenReturn(Optional.of(mockedJobTarget)); -// -// // when -// gameService.changeStatus(); -// -// // then -// Mockito.verify(sseEmitterRepository, Mockito.times(1)).get(STATUSCHANGEDGAME.getCode()); -// Mockito.verify(sseEmitterRepository, Mockito.times(0)).get(NOTCHANGEDGAME.getCode()); -// } + @Test + void 상태가_변경시_이벤트가_발행된다() throws IOException { + // given + JobTarget mockedJobTarget = Mockito.mock(JobTarget.class); + Mockito.when(sseEmitterRepository.findByCode(any())).thenReturn(List.of()); + Mockito.when(jobTargetRepository.findById(any())).thenReturn(Optional.of(mockedJobTarget)); + + // when + gameService.changeStatus(); + + // then + Mockito.verify(sseEmitterRepository, Mockito.times(1)).findByCode(STATUSCHANGEDGAME.getCode()); + Mockito.verify(sseEmitterRepository, Mockito.times(0)).findByCode(NOTCHANGEDGAME.getCode()); + } } diff --git a/src/test/java/mafia/mafiatogether/game/ui/GameControllerTest.java b/src/test/java/mafia/mafiatogether/game/ui/GameControllerTest.java index 12f4a97a..8104c150 100644 --- a/src/test/java/mafia/mafiatogether/game/ui/GameControllerTest.java +++ b/src/test/java/mafia/mafiatogether/game/ui/GameControllerTest.java @@ -1,12 +1,7 @@ package mafia.mafiatogether.game.ui; -import static org.assertj.core.api.SoftAssertions.assertSoftly; -import static org.hamcrest.Matchers.equalTo; - import io.restassured.RestAssured; import io.restassured.http.ContentType; -import java.util.Base64; -import java.util.Map; import mafia.mafiatogether.common.exception.ErrorResponse; import mafia.mafiatogether.common.exception.ExceptionCode; import mafia.mafiatogether.game.application.dto.response.GameInfoResponse; @@ -22,6 +17,12 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; +import java.util.Base64; +import java.util.Map; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.hamcrest.Matchers.equalTo; + @SuppressWarnings("NonAsciiCharacters") class GameControllerTest extends ControllerTest { diff --git a/src/test/java/mafia/mafiatogether/global/ControllerTest.java b/src/test/java/mafia/mafiatogether/global/ControllerTest.java index f75f6ff6..f7a0068e 100644 --- a/src/test/java/mafia/mafiatogether/global/ControllerTest.java +++ b/src/test/java/mafia/mafiatogether/global/ControllerTest.java @@ -2,8 +2,6 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; -import java.util.Base64; -import java.util.Map; import mafia.mafiatogether.game.domain.GameRepository; import mafia.mafiatogether.game.domain.PlayerCollection; import mafia.mafiatogether.game.domain.status.StatusType; @@ -13,20 +11,13 @@ import mafia.mafiatogether.lobby.domain.LobbyRepository; import mafia.mafiatogether.vote.domain.VoteRepository; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; -@Import(RedisTestConfig.class) -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -public abstract class ControllerTest { +import java.util.Base64; +import java.util.Map; - @LocalServerPort - private int port; +public abstract class ControllerTest extends RedisTestContainerSpringBootTest{ @Autowired protected LobbyRepository lobbyRepository; @@ -49,10 +40,6 @@ public abstract class ControllerTest { protected String POLICE; protected String CITIZEN; - @BeforeEach - void setPort() { - RestAssured.port = port; - } protected void setLobby() { final Lobby lobby = Lobby.create(CODE, LobbyInfo.of(5, 2, 1, 1)); diff --git a/src/test/java/mafia/mafiatogether/global/RedisTestConfig.java b/src/test/java/mafia/mafiatogether/global/RedisTestConfig.java index b2c618b3..56119f40 100644 --- a/src/test/java/mafia/mafiatogether/global/RedisTestConfig.java +++ b/src/test/java/mafia/mafiatogether/global/RedisTestConfig.java @@ -4,6 +4,8 @@ import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; +import java.time.Duration; + @TestConfiguration public class RedisTestConfig { @@ -12,11 +14,12 @@ public class RedisTestConfig { static { GenericContainer REDIS_CONTAINER = new GenericContainer<>(DockerImageName.parse(REDIS_DOCKER_IMAGE)) .withExposedPorts(6379) + .withStartupTimeout(Duration.ofSeconds(60)) .withReuse(true); REDIS_CONTAINER.start(); - System.setProperty("spring.redis.host", REDIS_CONTAINER.getHost()); - System.setProperty("spring.redis.port", REDIS_CONTAINER.getMappedPort(6379).toString()); + System.setProperty("spring.data.redis.host", REDIS_CONTAINER.getHost()); + System.setProperty("spring.data.redis.port", REDIS_CONTAINER.getMappedPort(6379).toString()); } } diff --git a/src/test/java/mafia/mafiatogether/global/RedisTestContainerSpringBootTest.java b/src/test/java/mafia/mafiatogether/global/RedisTestContainerSpringBootTest.java new file mode 100644 index 00000000..7cfaed82 --- /dev/null +++ b/src/test/java/mafia/mafiatogether/global/RedisTestContainerSpringBootTest.java @@ -0,0 +1,22 @@ +package mafia.mafiatogether.global; + +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.TestPropertySource; + +@Import(RedisTestConfig.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = {"application.scheduling-enable=false"}) +public abstract class RedisTestContainerSpringBootTest { + + @LocalServerPort + private int port; + + @BeforeEach + void setPort() { + RestAssured.port = port; + } +} diff --git a/src/test/java/mafia/mafiatogether/lobby/application/LobbyRemoveTest.java b/src/test/java/mafia/mafiatogether/lobby/application/LobbyRemoveTest.java index 5240beae..48282be6 100644 --- a/src/test/java/mafia/mafiatogether/lobby/application/LobbyRemoveTest.java +++ b/src/test/java/mafia/mafiatogether/lobby/application/LobbyRemoveTest.java @@ -12,9 +12,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.TestPropertySource; @SpringBootTest @SuppressWarnings("NonAsciiCharacters") +@TestPropertySource(properties = {"application.scheduling-enable=false"}) class LobbyRemoveTest { @MockBean