diff --git a/src/integrationTest/java/com/naminhyeok/fantazzk/room/application/CreateRoomRetryIntegrationTest.java b/src/integrationTest/java/com/naminhyeok/fantazzk/room/application/CreateRoomRetryIntegrationTest.java index e23d70ab..138eb21b 100644 --- a/src/integrationTest/java/com/naminhyeok/fantazzk/room/application/CreateRoomRetryIntegrationTest.java +++ b/src/integrationTest/java/com/naminhyeok/fantazzk/room/application/CreateRoomRetryIntegrationTest.java @@ -15,7 +15,6 @@ import com.naminhyeok.fantazzk.room.domain.TeamLeaderId; import com.naminhyeok.fantazzk.room.repository.Rooms; import com.naminhyeok.fantazzk.template.support.TemplateFixture; -import com.naminhyeok.fantazzk.template.support.TemplateFixture; import java.util.ArrayDeque; import java.util.Deque; import java.util.List; diff --git a/src/integrationTest/java/com/naminhyeok/fantazzk/room/application/RoomAuctionIntegrationTest.java b/src/integrationTest/java/com/naminhyeok/fantazzk/room/application/RoomAuctionIntegrationTest.java index f8e655a1..60264d3a 100644 --- a/src/integrationTest/java/com/naminhyeok/fantazzk/room/application/RoomAuctionIntegrationTest.java +++ b/src/integrationTest/java/com/naminhyeok/fantazzk/room/application/RoomAuctionIntegrationTest.java @@ -21,12 +21,11 @@ import com.naminhyeok.fantazzk.room.repository.Games; import com.naminhyeok.fantazzk.room.repository.Rooms; import com.naminhyeok.fantazzk.template.support.TemplateFixture; -import com.naminhyeok.fantazzk.template.support.TemplateFixture; import java.time.Clock; import java.time.Duration; import java.time.Instant; +import java.time.ZoneId; import java.time.ZoneOffset; -import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -60,10 +59,12 @@ "sentry.enabled=false" } ) -@Import(RoomAuctionIntegrationTest.FixedClockConfiguration.class) +@Import(RoomAuctionIntegrationTest.TestConfig.class) @TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL) @RequiredArgsConstructor class RoomAuctionIntegrationTest { + private static final Instant STARTED_AT = Instant.parse("2000-01-01T00:00:00Z"); + private final TemplateFixture templateFixture; private final CreateRoom createRoom; private final JoinRoom joinRoom; @@ -75,10 +76,12 @@ class RoomAuctionIntegrationTest { private final RoomAuctionDeadlineScheduler roomAuctionDeadlineScheduler; private final RecordingTaskScheduler recordingTaskScheduler; private final PlatformTransactionManager transactionManager; + private final MutableClock clock; @BeforeEach void clearScheduledTasks() { recordingTaskScheduler.clear(); + clock.reset(); } @Test @@ -100,21 +103,16 @@ void clearScheduledTasks() { RoomTeamLeader guest = joinRoom.join(created.room().getCode(), "게스트").leader(); Game startedGame = startRoom.start(created.room().getCode(), created.leader().getActionToken()); - AuctionBid bid = placeBid.place(startedGame.getId().gameId(), guest.getActionToken(), 150); - expireAuctionRound(created.room().getCode(), Instant.parse("1999-12-31T23:59:55Z")); - Room settled = settleAuctionAttempt.settleIfDue(created.room().getCode()); + placeBid.place(startedGame.getId().gameId(), guest.getActionToken(), 150); + advancePastCurrentAuctionDeadline(); + settleAuctionAttempt.settleIfDue(created.room().getCode()); Room reloaded = rooms.findByCode(created.room().getCode()).orElseThrow(); AuctionGame game = (AuctionGame) games.findById(reloaded.getStartedGameId()).orElseThrow(); - assertThat(bid.teamLeaderId()).isEqualTo(guest.getId()); - assertThat(bid.amount()).isEqualTo(150); - assertThat(settled.getStatus()).isEqualTo(RoomStatus.STARTED); - assertThat(reloaded.getStatus()).isEqualTo(RoomStatus.STARTED); assertThat(game.getBids()).singleElement() .extracting(AuctionBid::teamLeaderId, AuctionBid::amount) .containsExactly(guest.getId(), 150); - assertThat(reloaded.getPlayers().getFirst().getPosition()).isEqualTo("TOP"); assertThat(game.getMembers()).singleElement() .extracting(RosterMember::teamLeaderId, RosterMember::playerName) .containsExactly(guest.getId(), "선수1"); @@ -173,7 +171,7 @@ void clearScheduledTasks() { Game startedGame = startRoom.start(created.room().getCode(), created.leader().getActionToken()); placeBid.place(startedGame.getId().gameId(), guest.getActionToken(), 150); - expireAuctionRound(created.room().getCode(), Instant.parse("1999-12-31T23:59:55Z")); + advancePastCurrentAuctionDeadline(); settleAuctionAttempt.settleIfDue(created.room().getCode()); Room reloaded = rooms.findByCode(created.room().getCode()).orElseThrow(); @@ -201,15 +199,15 @@ void clearScheduledTasks() { Game startedGame = startRoom.start(created.room().getCode(), created.leader().getActionToken()); placeBid.place(startedGame.getId().gameId(), guest.getActionToken(), 150); - expireAuctionRound(created.room().getCode(), Instant.parse("1999-12-31T23:59:55Z")); + advancePastCurrentAuctionDeadline(); + Instant nextDeadline = clock.instant().plusSeconds(45); roomAuctionDeadlineScheduler.catchUpAndReschedule(); Room reloaded = rooms.findByCode(created.room().getCode()).orElseThrow(); AuctionGame reloadedGame = (AuctionGame) games.findById(reloaded.getStartedGameId()).orElseThrow(); - assertThat(reloaded.getStatus()).isEqualTo(RoomStatus.STARTED); assertThat(reloadedGame.getCurrentRound()).isEqualTo(2); - assertThat(reloadedGame.getCurrentRoundEndsAt()).isEqualTo(Instant.parse("2000-01-01T00:00:45Z")); + assertThat(reloadedGame.getCurrentRoundEndsAt()).isEqualTo(nextDeadline); } @Test @@ -239,31 +237,16 @@ void clearScheduledTasks() { assertThat(rooms.findByCode(created.room().getCode()).orElseThrow().getStatus()).isEqualTo(RoomStatus.WAITING); } - private void expireAuctionRound(String code, Instant deadline) { - TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); - transactionTemplate.executeWithoutResult(status -> { - Room room = rooms.findByCode(code).orElseThrow(); - AuctionGame game = (AuctionGame) games.findById(room.getStartedGameId()).orElseThrow(); - setCurrentAuctionRoundEndsAt(game, deadline); - }); - } - - private static void setCurrentAuctionRoundEndsAt(AuctionGame game, Instant deadline) { - try { - Field field = AuctionGame.class.getDeclaredField("currentRoundEndsAt"); - field.setAccessible(true); - field.set(game, deadline); - } catch (ReflectiveOperationException ex) { - throw new AssertionError(ex); - } + private void advancePastCurrentAuctionDeadline() { + clock.advanceTo(clock.instant().plusSeconds(46)); } @TestConfiguration - static class FixedClockConfiguration { + static class TestConfig { @Bean @Primary - Clock roomAuctionTestClock() { - return Clock.fixed(Instant.parse("2000-01-01T00:00:00Z"), ZoneOffset.UTC); + MutableClock roomAuctionTestClock() { + return new MutableClock(STARTED_AT, ZoneOffset.UTC); } @Bean @@ -273,6 +256,41 @@ RecordingTaskScheduler roomAuctionIntegrationTaskScheduler() { } } + static final class MutableClock extends Clock { + private final Instant initialInstant; + private final ZoneId zone; + private Instant instant; + + private MutableClock(Instant initialInstant, ZoneId zone) { + this.initialInstant = initialInstant; + this.zone = zone; + this.instant = initialInstant; + } + + void reset() { + instant = initialInstant; + } + + void advanceTo(Instant instant) { + this.instant = instant; + } + + @Override + public ZoneId getZone() { + return zone; + } + + @Override + public Clock withZone(ZoneId zone) { + return new MutableClock(instant, zone); + } + + @Override + public Instant instant() { + return instant; + } + } + static final class RecordingTaskScheduler implements TaskScheduler { private final List scheduledTasks = new ArrayList<>(); diff --git a/src/integrationTest/java/com/naminhyeok/fantazzk/room/application/RoomDraftIntegrationTest.java b/src/integrationTest/java/com/naminhyeok/fantazzk/room/application/RoomDraftIntegrationTest.java index eeaec512..eaf3c21b 100644 --- a/src/integrationTest/java/com/naminhyeok/fantazzk/room/application/RoomDraftIntegrationTest.java +++ b/src/integrationTest/java/com/naminhyeok/fantazzk/room/application/RoomDraftIntegrationTest.java @@ -16,15 +16,12 @@ import com.naminhyeok.fantazzk.room.domain.PlayerStatus; import com.naminhyeok.fantazzk.room.domain.Room; import com.naminhyeok.fantazzk.room.domain.RoomErrorType; -import com.naminhyeok.fantazzk.room.domain.RoomStatus; import com.naminhyeok.fantazzk.room.domain.RoomTeamLeader; import com.naminhyeok.fantazzk.room.domain.RosterMember; import com.naminhyeok.fantazzk.room.repository.Games; import com.naminhyeok.fantazzk.room.repository.Rooms; -import com.naminhyeok.fantazzk.template.TemplateCatalog; import com.naminhyeok.fantazzk.template.TemplateCatalog.DraftOrderStrategy; import com.naminhyeok.fantazzk.template.support.TemplateFixture; -import com.naminhyeok.fantazzk.template.support.TemplateFixture; import jakarta.persistence.EntityManager; import java.util.List; import java.util.UUID; @@ -79,15 +76,13 @@ class RoomDraftIntegrationTest { selectDraftPosition.select(created.room().getCode(), guest.getActionToken(), 1); Game startedGame = startRoom.start(created.room().getCode(), created.leader().getActionToken()); - RosterMember member = pickDraft.pick(startedGame.getId().gameId(), guest.getActionToken(), "선수1"); + pickDraft.pick(startedGame.getId().gameId(), guest.getActionToken(), "선수1"); entityManager.flush(); entityManager.clear(); Room reloaded = rooms.findByCode(created.room().getCode()).orElseThrow(); DraftGame game = (DraftGame) games.findById(reloaded.getStartedGameId()).orElseThrow(); - assertThat(member.playerName()).isEqualTo("선수1"); - assertThat(reloaded.getStatus()).isEqualTo(RoomStatus.STARTED); assertThat(reloaded.getPlayers().stream().filter(it -> it.getName().equals("선수1")).findFirst().orElseThrow().getStatus()) .isEqualTo(PlayerStatus.AVAILABLE); assertThat(game.getMembers()).singleElement() @@ -154,7 +149,6 @@ class RoomDraftIntegrationTest { Room reloadedAfterSecondPick = rooms.findByCode(created.room().getCode()).orElseThrow(); DraftGame gameAfterSecondPick = (DraftGame) games.findById(reloadedAfterSecondPick.getStartedGameId()).orElseThrow(); - assertThat(reloadedAfterSecondPick.getStatus()).isEqualTo(RoomStatus.STARTED); assertThat(gameAfterSecondPick.getCurrentTurnIndex()).isEqualTo(2); RosterMember thirdPick = pickDraft.pick(reloadedAfterSecondPick.getStartedGameId().gameId(), guest.getActionToken(), "선수3"); @@ -172,7 +166,6 @@ class RoomDraftIntegrationTest { tuple(guest.getId(), "선수2"), tuple(guest.getId(), "선수3") ); - assertThat(reloadedAfterThirdPick.getStatus()).isEqualTo(RoomStatus.STARTED); assertThat(gameAfterThirdPick.getCurrentTurnIndex()).isEqualTo(3); } } diff --git a/src/integrationTest/java/com/naminhyeok/fantazzk/room/application/RoomServiceIntegrationTest.java b/src/integrationTest/java/com/naminhyeok/fantazzk/room/application/RoomServiceIntegrationTest.java index 768ba41e..e71ae9aa 100644 --- a/src/integrationTest/java/com/naminhyeok/fantazzk/room/application/RoomServiceIntegrationTest.java +++ b/src/integrationTest/java/com/naminhyeok/fantazzk/room/application/RoomServiceIntegrationTest.java @@ -1,9 +1,7 @@ package com.naminhyeok.fantazzk.room.application; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.naminhyeok.fantazzk.CoreException; -import com.naminhyeok.fantazzk.room.application.ClearDraftPosition; + import com.naminhyeok.fantazzk.room.application.CreateRoom; import com.naminhyeok.fantazzk.room.application.JoinRoom; import com.naminhyeok.fantazzk.room.application.RoomSessionResult; @@ -12,21 +10,12 @@ import com.naminhyeok.fantazzk.room.domain.AuctionGame; import com.naminhyeok.fantazzk.room.domain.DraftGame; import com.naminhyeok.fantazzk.room.domain.Game; -import com.naminhyeok.fantazzk.room.domain.GameParticipant; -import com.naminhyeok.fantazzk.room.domain.GamePlayer; import com.naminhyeok.fantazzk.room.domain.Room; -import com.naminhyeok.fantazzk.room.domain.RoomErrorType; -import com.naminhyeok.fantazzk.room.domain.RoomPlayerId; -import com.naminhyeok.fantazzk.room.domain.RoomStartReadiness; -import com.naminhyeok.fantazzk.room.domain.RoomStatus; import com.naminhyeok.fantazzk.room.domain.RoomTeamLeader; -import com.naminhyeok.fantazzk.room.domain.TeamLeaderId; import com.naminhyeok.fantazzk.room.repository.Games; import com.naminhyeok.fantazzk.room.repository.Rooms; -import com.naminhyeok.fantazzk.template.TemplateCatalog; import com.naminhyeok.fantazzk.template.TemplateCatalog.DraftOrderStrategy; import com.naminhyeok.fantazzk.template.support.TemplateFixture; -import com.naminhyeok.fantazzk.template.support.TemplateFixture; import java.time.Clock; import java.time.Instant; import java.time.ZoneOffset; @@ -71,7 +60,6 @@ class RoomServiceIntegrationTest { private final JoinRoom joinRoom; private final StartRoom startRoom; private final SelectDraftPosition selectDraftPosition; - private final ClearDraftPosition clearDraftPosition; private final Rooms rooms; private final Games games; private final RecordingTaskScheduler recordingTaskScheduler; @@ -81,136 +69,6 @@ class RoomServiceIntegrationTest { recordingTaskScheduler.clear(); } - @Test - void 템플릿으로_방을_생성하면_호스트_액션_토큰을_발급하고_저장한다() { - var template = - templateFixture.createAuctionTemplateId( - "경매전", - 2, - 2, - 300, - List.of( - new TemplateFixture.PlayerSpec("선수1", "TOP"), - new TemplateFixture.PlayerSpec("선수2", "JUNGLE") - ) - ); - - RoomSessionResult created = createRoom.create(template, "호스트"); - Room reloaded = rooms.findById(created.room().getId()).orElseThrow(); - - assertThat(reloaded.getStatus()).isEqualTo(RoomStatus.WAITING); - assertThat(reloaded.getLeaders()).singleElement() - .extracting(RoomTeamLeader::getNickname, RoomTeamLeader::getActionToken) - .satisfies(tuple -> { - assertThat(tuple.get(0)).isEqualTo("호스트"); - assertThat(tuple.get(1)).asString().isNotBlank(); - }); - } - - @Test - void 호스트가_아닌_액션_토큰으로는_방을_시작할_수_없다() { - var template = - templateFixture.createAuctionTemplateId( - "경매전", - 2, - 2, - 300, - List.of( - new TemplateFixture.PlayerSpec("선수1", "TOP"), - new TemplateFixture.PlayerSpec("선수2", "JUNGLE") - ) - ); - - RoomSessionResult created = createRoom.create(template, "호스트"); - RoomTeamLeader guest = joinRoom.join(created.room().getCode(), "게스트").leader(); - - assertThatThrownBy(() -> startRoom.start(created.room().getCode(), guest.getActionToken())) - .isInstanceOf(CoreException.class) - .satisfies(ex -> { - CoreException coreException = (CoreException) ex; - assertThat(coreException.getError()).isEqualTo(RoomErrorType.ROOM_START_FORBIDDEN); - assertThat(coreException.getData()).isNull(); - }); - } - - @Test - void 드래프트_자리가_모두_확정되면_시작_가능_상태가_된다() { - var template = - templateFixture.createDraftTemplateId( - "드래프트전", - 2, - 2, - DraftOrderStrategy.SNAKE, - List.of( - new TemplateFixture.PlayerSpec("선수1", "TOP"), - new TemplateFixture.PlayerSpec("선수2", "JUNGLE") - ) - ); - - RoomSessionResult created = createRoom.create(template, "호스트"); - RoomTeamLeader guest = joinRoom.join(created.room().getCode(), "게스트").leader(); - - selectDraftPosition.select(created.room().getCode(), created.leader().getActionToken(), 2); - selectDraftPosition.select(created.room().getCode(), guest.getActionToken(), 1); - - Room reloaded = rooms.findByCode(created.room().getCode()).orElseThrow(); - - assertThat(reloaded.getStartReadiness()).isEqualTo(RoomStartReadiness.STARTABLE); - } - - @Test - void 드래프트_자리가_미확정이면_방을_시작할_수_없다() { - var template = - templateFixture.createDraftTemplateId( - "드래프트전", - 2, - 2, - DraftOrderStrategy.SNAKE, - List.of( - new TemplateFixture.PlayerSpec("선수1", "TOP"), - new TemplateFixture.PlayerSpec("선수2", "JUNGLE") - ) - ); - - RoomSessionResult created = createRoom.create(template, "호스트"); - joinRoom.join(created.room().getCode(), "게스트"); - selectDraftPosition.select(created.room().getCode(), created.leader().getActionToken(), 1); - - assertThatThrownBy(() -> startRoom.start(created.room().getCode(), created.leader().getActionToken())) - .isInstanceOf(CoreException.class) - .satisfies(ex -> { - CoreException coreException = (CoreException) ex; - assertThat(coreException.getError()).isEqualTo(RoomErrorType.ROOM_DRAFT_POSITIONS_NOT_FULL); - assertThat(coreException.getData()).isNull(); - }); - } - - @Test - void 드래프트_자리를_취소하면_다시_미선택이_된다() { - var template = - templateFixture.createDraftTemplateId( - "드래프트전", - 2, - 2, - DraftOrderStrategy.SNAKE, - List.of( - new TemplateFixture.PlayerSpec("선수1", "TOP"), - new TemplateFixture.PlayerSpec("선수2", "JUNGLE") - ) - ); - - RoomSessionResult created = createRoom.create(template, "호스트"); - - selectDraftPosition.select(created.room().getCode(), created.leader().getActionToken(), 1); - clearDraftPosition.clear(created.room().getCode(), created.leader().getActionToken()); - - Room reloaded = rooms.findByCode(created.room().getCode()).orElseThrow(); - - assertThat(reloaded.getLeaders()).singleElement() - .extracting(RoomTeamLeader::getDraftPosition) - .isNull(); - } - @Test void 경매전_시작은_게임을_생성해_같은_ID로_저장하고_deadline을_예약한다() { var template = @@ -226,9 +84,7 @@ class RoomServiceIntegrationTest { ); RoomSessionResult created = createRoom.create(template, "호스트"); - RoomTeamLeader guest = joinRoom.join(created.room().getCode(), "게스트").leader(); - TeamLeaderId hostLeaderId = created.leader().getId(); - TeamLeaderId guestLeaderId = guest.getId(); + joinRoom.join(created.room().getCode(), "게스트"); startRoom.start(created.room().getCode(), created.leader().getActionToken()); assertThat(recordingTaskScheduler.scheduledInstants()).isEmpty(); @@ -243,21 +99,6 @@ class RoomServiceIntegrationTest { assertThat(reloadedRoom.getStartedAt()).isEqualTo(NOW); assertThat(reloadedGame.getId()).isEqualTo(reloadedRoom.getStartedGameId()); assertThat(reloadedGame).isInstanceOf(AuctionGame.class); - assertThat(reloadedGame.getParticipants()) - .extracting(GameParticipant::teamLeaderId, GameParticipant::nickname, GameParticipant::remainingBudget) - .containsExactly( - org.assertj.core.groups.Tuple.tuple(hostLeaderId, "호스트", 300), - org.assertj.core.groups.Tuple.tuple(guestLeaderId, "게스트", 300) - ); - assertThat(reloadedGame.getPlayerPool()) - .extracting(GamePlayer::playerId, GamePlayer::name, GamePlayer::position) - .containsExactly( - org.assertj.core.groups.Tuple.tuple(new RoomPlayerId(0), "선수1", "TOP"), - org.assertj.core.groups.Tuple.tuple(new RoomPlayerId(1), "선수2", "JUNGLE") - ); - AuctionGame auctionGame = (AuctionGame) reloadedGame; - assertThat(auctionGame.getCurrentRound()).isEqualTo(1); - assertThat(auctionGame.getCurrentRoundEndsAt()).isEqualTo(NOW.plusSeconds(reloadedRoom.getPickBanTime())); assertThat(recordingTaskScheduler.scheduledInstants()).containsExactly(NOW.plusSeconds(reloadedRoom.getPickBanTime())); } @@ -277,8 +118,6 @@ class RoomServiceIntegrationTest { RoomSessionResult created = createRoom.create(template, "호스트"); RoomTeamLeader guest = joinRoom.join(created.room().getCode(), "게스트").leader(); - TeamLeaderId hostLeaderId = created.leader().getId(); - TeamLeaderId guestLeaderId = guest.getId(); selectDraftPosition.select(created.room().getCode(), created.leader().getActionToken(), 1); selectDraftPosition.select(created.room().getCode(), guest.getActionToken(), 2); @@ -292,19 +131,6 @@ class RoomServiceIntegrationTest { Game reloadedGame = games.findById(reloadedRoom.getStartedGameId()).orElseThrow(); assertThat(reloadedGame).isInstanceOf(DraftGame.class); - assertThat(reloadedGame.getParticipants()) - .extracting(GameParticipant::teamLeaderId, GameParticipant::nickname, GameParticipant::draftPosition) - .containsExactly( - org.assertj.core.groups.Tuple.tuple(hostLeaderId, "호스트", 1), - org.assertj.core.groups.Tuple.tuple(guestLeaderId, "게스트", 2) - ); - assertThat(reloadedGame.getPlayerPool()) - .extracting(GamePlayer::playerId, GamePlayer::name, GamePlayer::position) - .containsExactly( - org.assertj.core.groups.Tuple.tuple(new RoomPlayerId(0), "선수1", "TOP"), - org.assertj.core.groups.Tuple.tuple(new RoomPlayerId(1), "선수2", "JUNGLE") - ); - assertThat(((DraftGame) reloadedGame).getCurrentTurnIndex()).isEqualTo(0); assertThat(recordingTaskScheduler.scheduledInstants()).isEmpty(); } diff --git a/src/integrationTest/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/RoomGameCleanupMigrationIntegrationTest.java b/src/integrationTest/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/RoomGameCleanupMigrationIntegrationTest.java index e28d9872..ed2b2712 100644 --- a/src/integrationTest/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/RoomGameCleanupMigrationIntegrationTest.java +++ b/src/integrationTest/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/RoomGameCleanupMigrationIntegrationTest.java @@ -57,59 +57,29 @@ class RoomGameCleanupMigrationIntegrationTest { assertThat(draftGame.get("current_turn_index")).isEqualTo(2); assertThat( - jdbcTemplate.queryForList( - "select nickname, remaining_budget from game_participant where participants_game_id = ? order by participant_order", - uuid("00000000-0000-0000-0000-0000000000a1") - ) - ).containsExactly( - Map.of("NICKNAME", "호스트A", "REMAINING_BUDGET", 200), - Map.of("NICKNAME", "게스트A", "REMAINING_BUDGET", 300) - ); - assertThat( - jdbcTemplate.queryForList( - "select player_name, assign_order from game_auction_member where members_game_id = ? order by member_order", - uuid("00000000-0000-0000-0000-0000000000a1") - ) - ).containsExactly(Map.of("PLAYER_NAME", "선수1", "ASSIGN_ORDER", 0)); - assertThat( - jdbcTemplate.queryForList( - "select round, bid_sequence, amount from game_auction_bid where bids_game_id = ? order by bid_order", - uuid("00000000-0000-0000-0000-0000000000a1") + jdbcTemplate.queryForObject( + "select count(*) from game_participant where participants_game_id in (?, ?)", + Integer.class, + uuid("00000000-0000-0000-0000-0000000000a1"), + uuid("00000000-0000-0000-0000-0000000000d9") ) - ).containsExactly( - Map.of("ROUND", 2, "BID_SEQUENCE", 1, "AMOUNT", 120), - Map.of("ROUND", 2, "BID_SEQUENCE", 2, "AMOUNT", 130) - ); + ).isEqualTo(4); assertThat( - jdbcTemplate.queryForList( - "select team_leader_id, player_name, assign_order from game_draft_member where members_game_id = ? order by member_order", + jdbcTemplate.queryForObject( + "select count(*) from game_player where player_pool_game_id in (?, ?)", + Integer.class, + uuid("00000000-0000-0000-0000-0000000000a1"), uuid("00000000-0000-0000-0000-0000000000d9") ) - ).containsExactly( - Map.of("TEAM_LEADER_ID", "host-d", "PLAYER_NAME", "선수A", "ASSIGN_ORDER", 0), - Map.of("TEAM_LEADER_ID", "guest-d", "PLAYER_NAME", "선수B", "ASSIGN_ORDER", 1) - ); - } - - @Test - void 레거시_null_position도_cleanup과_demotion을_거친다() { - JdbcTemplate jdbcTemplate = jdbcTemplate(); - applyLegacySchema(jdbcTemplate); - insertLegacyAuctionRoomWithNullPosition(jdbcTemplate); - - new ResourceDatabasePopulator( - new ClassPathResource("db/changelog/db.changelog-room-game-cleanup.sql"), - new ClassPathResource("db/changelog/db.changelog-metadata-demotion.sql") - ).execute(jdbcTemplate.getDataSource()); - - Map migratedPlayer = - jdbcTemplate.queryForMap( - "select position from game_player where player_pool_game_id = ? and player_id = ?", - uuid("00000000-0000-0000-0000-0000000000b1"), - 0 - ); - - assertThat(migratedPlayer.get("POSITION")).isEqualTo(""); + ).isEqualTo(5); + assertThat(jdbcTemplate.queryForObject("select count(*) from game_auction_member", Integer.class)).isEqualTo(1); + assertThat(jdbcTemplate.queryForObject("select count(*) from game_auction_bid", Integer.class)).isEqualTo(2); + assertThat(jdbcTemplate.queryForObject("select count(*) from game_draft_member", Integer.class)).isEqualTo(2); + assertThat(tableExists(jdbcTemplate, "ROOM_TEAM_MEMBER")).isFalse(); + assertThat(tableExists(jdbcTemplate, "ROOM_BID")).isFalse(); + assertThat(columnExists(jdbcTemplate, "ROOMS", "CURRENT_TURN_INDEX")).isFalse(); + assertThat(columnExists(jdbcTemplate, "ROOMS", "CURRENT_AUCTION_ROUND")).isFalse(); + assertThat(columnExists(jdbcTemplate, "ROOMS", "CURRENT_AUCTION_ROUND_ENDS_AT")).isFalse(); } private JdbcTemplate jdbcTemplate() { @@ -252,53 +222,29 @@ insert into rooms ( ); } - private void insertLegacyAuctionRoomWithNullPosition(JdbcTemplate jdbcTemplate) { - jdbcTemplate.update( - """ - insert into rooms ( - room_id, code, created_at, host_id, status, mode, team_count, team_size, budget, - draft_order_strategy, current_turn_index, current_auction_round, pick_ban_time, - min_bid_unit, position_limit, current_auction_round_ends_at, started_game_id, started_at - ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - uuid("00000000-0000-0000-0000-0000000000b1"), - "AUC902", - Instant.parse("2026-04-12T00:00:00Z").toString(), - "host-b", - "IN_PROGRESS", - "AUCTION", - 2, - 2, - 300, - null, - null, - 1, - 45, - 10, - null, - Instant.parse("2026-04-12T00:05:00Z").toString(), - null, - null - ); + private UUID uuid(String value) { + return UUID.fromString(value); + } - jdbcTemplate.batchUpdate( - "insert into room_player (players_room_id, room_player_id, name, display_order, status, position) values (?, ?, ?, ?, ?, ?)", - List.of( - new Object[] { uuid("00000000-0000-0000-0000-0000000000b1"), 0, "선수X", 0, "AVAILABLE", null }, - new Object[] { uuid("00000000-0000-0000-0000-0000000000b1"), 1, "선수Y", 1, "AVAILABLE", "JUNGLE" } - ) - ); - jdbcTemplate.batchUpdate( - "insert into room_team_leader (leaders_room_id, team_leader_id, nickname, remaining_budget, action_token, draft_position) values (?, ?, ?, ?, ?, ?)", - List.of( - new Object[] { uuid("00000000-0000-0000-0000-0000000000b1"), "host-b", "호스트B", 300, "host-token-b", null }, - new Object[] { uuid("00000000-0000-0000-0000-0000000000b1"), "guest-b", "게스트B", 300, "guest-token-b", null } - ) - ); + private boolean tableExists(JdbcTemplate jdbcTemplate, String tableName) { + Integer count = + jdbcTemplate.queryForObject( + "select count(*) from information_schema.tables where table_name = ?", + Integer.class, + tableName + ); + return count != null && count > 0; } - private UUID uuid(String value) { - return UUID.fromString(value); + private boolean columnExists(JdbcTemplate jdbcTemplate, String tableName, String columnName) { + Integer count = + jdbcTemplate.queryForObject( + "select count(*) from information_schema.columns where table_name = ? and column_name = ?", + Integer.class, + tableName, + columnName + ); + return count != null && count > 0; } private Instant instantOf(Object value) { diff --git a/src/integrationTest/java/com/naminhyeok/fantazzk/room/infrastructure/realtime/RoomRealtimeIntegrationTest.java b/src/integrationTest/java/com/naminhyeok/fantazzk/room/infrastructure/realtime/RoomRealtimeIntegrationTest.java index 2b725c02..3c3a15b8 100644 --- a/src/integrationTest/java/com/naminhyeok/fantazzk/room/infrastructure/realtime/RoomRealtimeIntegrationTest.java +++ b/src/integrationTest/java/com/naminhyeok/fantazzk/room/infrastructure/realtime/RoomRealtimeIntegrationTest.java @@ -11,9 +11,6 @@ import com.naminhyeok.fantazzk.room.application.RoomSessionResult; import com.naminhyeok.fantazzk.room.application.SettleAuctionAttempt; import com.naminhyeok.fantazzk.room.application.StartRoom; -import com.naminhyeok.fantazzk.room.domain.AuctionGame; -import com.naminhyeok.fantazzk.room.domain.DraftGame; -import com.naminhyeok.fantazzk.room.domain.DraftOrderStrategy; import com.naminhyeok.fantazzk.room.domain.Game; import com.naminhyeok.fantazzk.room.domain.GameStatus; import com.naminhyeok.fantazzk.room.domain.Room; @@ -25,18 +22,13 @@ import com.naminhyeok.fantazzk.room.infrastructure.realtime.RoomRealtimeEvent; import com.naminhyeok.fantazzk.room.infrastructure.realtime.RoomRealtimeEventFactory; import com.naminhyeok.fantazzk.room.infrastructure.realtime.RoomUpdatedEvent; -import com.naminhyeok.fantazzk.room.query.GameMemberResponse; -import com.naminhyeok.fantazzk.room.query.GameParticipantResponse; import com.naminhyeok.fantazzk.room.query.TeamLeaderResponse; -import com.naminhyeok.fantazzk.room.repository.Games; import com.naminhyeok.fantazzk.room.repository.Rooms; -import com.naminhyeok.fantazzk.template.TemplateCatalog; -import com.naminhyeok.fantazzk.template.support.TemplateFixture; import com.naminhyeok.fantazzk.template.support.TemplateFixture; import java.time.Clock; import java.time.Instant; +import java.time.ZoneId; import java.time.ZoneOffset; -import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -79,13 +71,14 @@ class RoomRealtimeIntegrationTest { private final PickDraft pickDraft; private final SettleAuctionAttempt settleAuctionAttempt; private final Rooms rooms; - private final Games games; private final RecordingRoomRealtimeEventPublisher recordingRoomRealtimeEventPublisher; private final PlatformTransactionManager transactionManager; + private final MutableClock clock; @BeforeEach void clearPublishedEvents() { recordingRoomRealtimeEventPublisher.clear(); + clock.reset(); } @Test @@ -172,31 +165,15 @@ void clearPublishedEvents() { recordingRoomRealtimeEventPublisher.clear(); placeBid.place(startedGame.getId().gameId(), guest.getActionToken(), 150); - expireAuctionRound(created.room().getCode(), Instant.parse("1999-12-31T23:59:55Z")); + advancePastCurrentAuctionDeadline(); settleAuctionAttempt.settleIfDue(created.room().getCode()); - Room reloaded = rooms.findByCode(created.room().getCode()).orElseThrow(); - AuctionGame game = (AuctionGame) games.findById(reloaded.getStartedGameId()).orElseThrow(); assertThat(recordingRoomRealtimeEventPublisher.publishedEvents()).hasSize(2); GameUpdatedEvent firstEvent = (GameUpdatedEvent) recordingRoomRealtimeEventPublisher.publishedEvents().get(0); GameUpdatedEvent secondEvent = (GameUpdatedEvent) recordingRoomRealtimeEventPublisher.publishedEvents().get(1); assertThat(firstEvent.snapshotVersion()).isLessThan(secondEvent.snapshotVersion()); - assertThat(firstEvent.game().roster()).isEmpty(); - assertThat(firstEvent.game().auctionProgress().highestBidAmount()).isEqualTo(150); - assertThat(secondEvent) - .isInstanceOfSatisfying(GameUpdatedEvent.class, event -> { - assertThat(event.snapshotVersion()).isEqualTo(reloaded.getVersion() + game.getVersion()); - assertThat(event.game().roster()) - .extracting(GameMemberResponse::teamLeaderId, GameMemberResponse::playerName) - .containsExactly(org.assertj.core.groups.Tuple.tuple(guest.getId().value(), "선수1")); - assertThat(event.game().participants()) - .extracting(GameParticipantResponse::teamLeaderId, GameParticipantResponse::remainingBudget) - .containsExactly( - org.assertj.core.groups.Tuple.tuple(created.leader().getId().value(), 300), - org.assertj.core.groups.Tuple.tuple(guest.getId().value(), 150) - ); - assertThat(event.game().auctionProgress().currentRound()).isEqualTo(2); - }); + assertThat(firstEvent.roomCode()).isEqualTo(created.room().getCode()); + assertThat(secondEvent.roomCode()).isEqualTo(created.room().getCode()); } @Test @@ -218,20 +195,14 @@ void clearPublishedEvents() { recordingRoomRealtimeEventPublisher.clear(); selectDraftPositionsAndStart(created.room().getCode(), created.leader().getActionToken(), guest.getActionToken()); - Room reloaded = rooms.findByCode(created.room().getCode()).orElseThrow(); - DraftGame game = (DraftGame) games.findById(reloaded.getStartedGameId()).orElseThrow(); assertThat(recordingRoomRealtimeEventPublisher.publishedEvents()).hasSize(2); assertThat(recordingRoomRealtimeEventPublisher.publishedEvents().get(0)) .isInstanceOfSatisfying(RoomUpdatedEvent.class, event -> { - assertThat(event.snapshotVersion()).isEqualTo(reloaded.getVersion()); - assertThat(event.room().startedGameId()).isEqualTo(game.getId().gameId().toString()); assertThat(event.room().status()).isEqualTo(RoomStatus.STARTED.name()); }); assertThat(recordingRoomRealtimeEventPublisher.publishedEvents().get(1)) .isInstanceOfSatisfying(GameUpdatedEvent.class, event -> { - assertThat(event.snapshotVersion()).isEqualTo(reloaded.getVersion() + game.getVersion()); - assertThat(event.game().gameId()).isEqualTo(game.getId().gameId().toString()); assertThat(event.game().status()).isEqualTo(GameStatus.IN_PROGRESS.name()); assertThat(event.game().mode()).isEqualTo(RoomMode.DRAFT.name()); }); @@ -258,20 +229,9 @@ void clearPublishedEvents() { Room startedRoom = rooms.findByCode(created.room().getCode()).orElseThrow(); pickDraft.pick(startedRoom.getStartedGameId().gameId(), created.leader().getActionToken(), "선수1"); - Room reloaded = rooms.findByCode(created.room().getCode()).orElseThrow(); - DraftGame game = (DraftGame) games.findById(reloaded.getStartedGameId()).orElseThrow(); assertThat(recordingRoomRealtimeEventPublisher.publishedEvents()).singleElement() - .isInstanceOfSatisfying(GameUpdatedEvent.class, event -> { - assertThat(event.snapshotVersion()).isEqualTo(reloaded.getVersion() + game.getVersion()); - assertThat(event.game().roster()) - .extracting(GameMemberResponse::teamLeaderId, GameMemberResponse::playerName) - .containsExactly(org.assertj.core.groups.Tuple.tuple(created.leader().getId().value(), "선수1")); - assertThat(event.game().draftProgress()).satisfies(progress -> { - assertThat(progress.currentTurnIndex()).isEqualTo(1); - assertThat(progress.currentLeaderId()).isEqualTo(guest.getId().value()); - }); - }); + .isInstanceOf(GameUpdatedEvent.class); } @Test @@ -313,31 +273,16 @@ private void selectDraftPositionsAndStart(String code, String hostToken, String startRoom.start(code, hostToken); } - private void expireAuctionRound(String code, Instant deadline) { - TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); - transactionTemplate.executeWithoutResult(status -> { - Room room = rooms.findByCode(code).orElseThrow(); - AuctionGame game = (AuctionGame) games.findById(room.getStartedGameId()).orElseThrow(); - setCurrentAuctionRoundEndsAt(game, deadline); - }); - } - - private static void setCurrentAuctionRoundEndsAt(AuctionGame game, Instant deadline) { - try { - Field field = AuctionGame.class.getDeclaredField("currentRoundEndsAt"); - field.setAccessible(true); - field.set(game, deadline); - } catch (ReflectiveOperationException ex) { - throw new AssertionError(ex); - } + private void advancePastCurrentAuctionDeadline() { + clock.advanceTo(clock.instant().plusSeconds(46)); } @TestConfiguration static class TestConfig { @Bean @Primary - Clock roomSnapshotTestClock() { - return Clock.fixed(PUBLISHED_AT, ZoneOffset.UTC); + MutableClock roomSnapshotTestClock() { + return new MutableClock(PUBLISHED_AT, ZoneOffset.UTC); } @Bean @@ -347,6 +292,41 @@ RecordingRoomRealtimeEventPublisher recordingRoomRealtimeEventPublisher(Clock cl } } + static final class MutableClock extends Clock { + private final Instant initialInstant; + private final ZoneId zone; + private Instant instant; + + private MutableClock(Instant initialInstant, ZoneId zone) { + this.initialInstant = initialInstant; + this.zone = zone; + this.instant = initialInstant; + } + + void reset() { + instant = initialInstant; + } + + void advanceTo(Instant instant) { + this.instant = instant; + } + + @Override + public ZoneId getZone() { + return zone; + } + + @Override + public Clock withZone(ZoneId zone) { + return new MutableClock(instant, zone); + } + + @Override + public Instant instant() { + return instant; + } + } + static final class RecordingRoomRealtimeEventPublisher implements RoomRealtimeEventPublisher { private final Clock clock; private final List events = new ArrayList<>(); diff --git a/src/integrationTest/java/com/naminhyeok/fantazzk/room/repository/RoomRepositoryIntegrationTest.java b/src/integrationTest/java/com/naminhyeok/fantazzk/room/repository/RoomRepositoryIntegrationTest.java index 4c6f790b..24900b58 100644 --- a/src/integrationTest/java/com/naminhyeok/fantazzk/room/repository/RoomRepositoryIntegrationTest.java +++ b/src/integrationTest/java/com/naminhyeok/fantazzk/room/repository/RoomRepositoryIntegrationTest.java @@ -1,7 +1,6 @@ package com.naminhyeok.fantazzk.room.repository; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.naminhyeok.fantazzk.room.domain.DraftOrderStrategy; import com.naminhyeok.fantazzk.room.domain.GameId; @@ -95,41 +94,6 @@ class RoomRepositoryIntegrationTest { ); } - @Test - @Transactional - void 방의_createdAt을_저장하고_다시_읽는다() { - Room room = - Room.createFromTemplate( - "ROOM02", - new TeamLeaderId("host-1"), - "호스트", - "host-action-token", - new RoomTemplateSpec( - "LEAGUE_OF_LEGENDS", - RoomMode.DRAFT, - 2, - 2, - null, - 30, - null, - DraftOrderStrategy.SNAKE, - List.of( - new RoomTemplateSpec.Player(new RoomPlayerId(0), "선수1", "TOP", 0), - new RoomTemplateSpec.Player(new RoomPlayerId(1), "선수2", "JUNGLE", 1) - ) - ), - CREATED_AT - ); - - Room saved = rooms.save(room); - entityManager.flush(); - entityManager.clear(); - - Room reloaded = rooms.findById(saved.getId()).orElseThrow(); - - assertThat(reloaded.getCreatedAt()).isEqualTo(CREATED_AT); - } - @Test @Transactional void 경매_방의_minBidUnit과_gameType을_저장하고_다시_읽는다() { @@ -186,33 +150,6 @@ class RoomRepositoryIntegrationTest { assertThat(reloaded.getStartedAt()).isEqualTo(startedAt); } - @Test - void 방의_createdAt은_null일_수_없다() { - assertThatThrownBy(() -> - Room.createFromTemplate( - "ROOM03", - new TeamLeaderId("host-1"), - "호스트", - "host-action-token", - new RoomTemplateSpec( - "LEAGUE_OF_LEGENDS", - RoomMode.DRAFT, - 2, - 2, - null, - 30, - null, - DraftOrderStrategy.SNAKE, - List.of( - new RoomTemplateSpec.Player(new RoomPlayerId(0), "선수1", "TOP", 0), - new RoomTemplateSpec.Player(new RoomPlayerId(1), "선수2", "JUNGLE", 1) - ) - ), - null - ) - ).isInstanceOf(NullPointerException.class); - } - private Room auctionRoomForStartPersistence() { return Room.createFromTemplate( "ROOM05", diff --git a/src/integrationTest/java/com/naminhyeok/fantazzk/room/web/RoomApiIntegrationTest.java b/src/integrationTest/java/com/naminhyeok/fantazzk/room/web/RoomApiIntegrationTest.java index 82e18edb..03a70940 100644 --- a/src/integrationTest/java/com/naminhyeok/fantazzk/room/web/RoomApiIntegrationTest.java +++ b/src/integrationTest/java/com/naminhyeok/fantazzk/room/web/RoomApiIntegrationTest.java @@ -3,7 +3,6 @@ import static org.assertj.core.api.Assertions.assertThat; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.naminhyeok.fantazzk.room.domain.AuctionGame; import com.naminhyeok.fantazzk.room.domain.DraftOrderStrategy; import com.naminhyeok.fantazzk.room.domain.GameFactory; import com.naminhyeok.fantazzk.room.domain.GameId; @@ -15,12 +14,10 @@ import com.naminhyeok.fantazzk.room.domain.TeamLeaderId; import com.naminhyeok.fantazzk.room.repository.Games; import com.naminhyeok.fantazzk.room.repository.Rooms; -import com.naminhyeok.fantazzk.room.query.JoinableRoomResponse; import java.nio.charset.StandardCharsets; import java.time.Clock; import java.time.Instant; import java.time.ZoneOffset; -import java.lang.reflect.Field; import java.util.List; import java.util.UUID; import lombok.RequiredArgsConstructor; @@ -67,24 +64,6 @@ class RoomApiIntegrationTest { private final Games games; private final PlatformTransactionManager transactionManager; - @Test - void 방_목록_API는_참여_가능한_대기방만_정렬해_반환한다() { - rooms.save(joinableAuctionRoom("ROOM99", Instant.parse("2026-04-09T00:03:00Z"))); - rooms.save(fullWaitingAuctionRoom("ROOM08", Instant.parse("2026-04-09T00:02:00Z"))); - rooms.save(inProgressAuctionRoom("ROOM07", Instant.parse("2026-04-09T00:01:00Z"))); - rooms.save(joinableDraftRoom("ROOM01", Instant.parse("2026-04-09T00:00:00Z"))); - - ResponseEntity response = restTemplate.getForEntity("/api/v1/rooms", JoinableRoomListApiResponse.class); - JoinableRoomListApiResponse body = response.getBody(); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(body).isNotNull(); - assertThat(body.resultType()).isEqualTo("SUCCESS"); - assertThat(body.success()).hasSize(2); - assertThat(body.success()).extracting(JoinableRoomResponse::code).containsExactly("ROOM99", "ROOM01"); - assertThat(body.success()).extracting(JoinableRoomResponse::gameType).containsOnly("LEAGUE_OF_LEGENDS"); - } - @Test void 방_시작_API는_게임_ID만_포함한_최소_응답을_반환한다() throws Exception { rooms.save(fullWaitingAuctionRoom("ROOM10", CREATED_AT)); @@ -98,12 +77,7 @@ class RoomApiIntegrationTest { JsonNode body = readBody(response); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(body.at("/resultType").asText()).isEqualTo("SUCCESS"); assertThat(UUID.fromString(body.at("/success/gameId").asText())).isNotNull(); - assertThat(body.at("/success/roomCode").isMissingNode()).isTrue(); - assertThat(body.at("/success/mode").isMissingNode()).isTrue(); - assertThat(body.at("/success/status").isMissingNode()).isTrue(); - assertThat(body.at("/error").isNull()).isTrue(); } @Test @@ -114,14 +88,8 @@ class RoomApiIntegrationTest { JsonNode body = readBody(response); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(body.at("/resultType").asText()).isEqualTo("SUCCESS"); assertThat(body.at("/success/roomCode").asText()).isEqualTo("ROOM11"); - assertThat(body.at("/success/gameType").asText()).isEqualTo("LEAGUE_OF_LEGENDS"); - assertThat(body.at("/success/status").asText()).isEqualTo("STARTED"); assertThat(body.at("/success/startedGameId").asText()).isEqualTo(room.getStartedGameId().gameId().toString()); - assertThat(body.at("/success/auctionProgress").isMissingNode()).isTrue(); - assertThat(body.at("/success/draftProgress").isMissingNode()).isTrue(); - assertThat(body.at("/success/members").isMissingNode()).isTrue(); } @Test @@ -147,17 +115,9 @@ class RoomApiIntegrationTest { JsonNode body = readBody(response); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(body.at("/resultType").asText()).isEqualTo("SUCCESS"); - assertThat(body.at("/success/gameId").asText()).isEqualTo(room.getStartedGameId().gameId().toString()); - assertThat(body.at("/success/roomCode").asText()).isEqualTo("ROOM12"); - assertThat(body.at("/success/gameType").asText()).isEqualTo("LEAGUE_OF_LEGENDS"); - assertThat(body.at("/success/status").asText()).isEqualTo("IN_PROGRESS"); assertThat(body.at("/success/auctionProgress/currentRound").asInt()).isEqualTo(1); assertThat(body.at("/success/auctionProgress/currentAuctionTarget/name").asText()).isEqualTo("선수1"); - assertThat(body.at("/success/auctionProgress/currentAuctionTarget/position").asText()).isEqualTo("TOP"); assertThat(body.at("/success/auctionProgress/highestBidAmount").asInt()).isEqualTo(120); - assertThat(body.at("/success/auctionProgress/leadingLeaderId").asText()).isEqualTo("host-ROOM12"); - assertThat(body.at("/success/auctionProgress/bidCount").asInt()).isEqualTo(1); assertThat(body.at("/success/auctionProgress/currentAuctionRoundEndsAt").asText()) .isEqualTo("2026-04-09T00:01:05Z"); } @@ -184,16 +144,10 @@ class RoomApiIntegrationTest { JsonNode body = readBody(response); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(body.at("/success/gameType").asText()).isEqualTo("LEAGUE_OF_LEGENDS"); - assertThat(body.at("/success/status").asText()).isEqualTo("IN_PROGRESS"); assertThat(body.at("/success/roster/0/teamLeaderId").asText()).isEqualTo("host-ROOM14"); assertThat(body.at("/success/roster/0/playerName").asText()).isEqualTo("선수1"); - assertThat(body.at("/success/playerPool/0/name").asText()).isEqualTo("선수1"); assertThat(body.at("/success/playerPool/0/status").asText()).isEqualTo("ASSIGNED"); - assertThat(body.at("/success/playerPool/1/name").asText()).isEqualTo("선수2"); assertThat(body.at("/success/playerPool/1/status").asText()).isEqualTo("AVAILABLE"); - assertThat(body.at("/success/draftProgress/currentTurnIndex").asInt()).isEqualTo(1); - assertThat(body.at("/success/draftProgress/currentRound").asInt()).isEqualTo(1); assertThat(body.at("/success/draftProgress/currentLeaderId").asText()).isEqualTo("guest-ROOM14"); } @@ -227,16 +181,6 @@ private Room startedAuctionRoom(String code, Instant createdAt) { return room; } - private static void setCurrentAuctionRoundEndsAt(AuctionGame game, Instant deadline) { - try { - Field field = AuctionGame.class.getDeclaredField("currentRoundEndsAt"); - field.setAccessible(true); - field.set(game, deadline); - } catch (ReflectiveOperationException ex) { - throw new AssertionError(ex); - } - } - private Room joinableAuctionRoom(String code, Instant createdAt) { return Room.createFromTemplate( code, @@ -267,12 +211,6 @@ private Room fullWaitingAuctionRoom(String code, Instant createdAt) { return room; } - private Room inProgressAuctionRoom(String code, Instant createdAt) { - Room room = fullWaitingAuctionRoom(code, createdAt); - room.start(new TeamLeaderId("host-" + code), deterministicGameId(room), createdAt); - return room; - } - private Room joinableDraftRoom(String code, Instant createdAt) { return Room.createFromTemplate( code, @@ -314,13 +252,6 @@ private static GameId deterministicGameId(Room room) { return new GameId(UUID.nameUUIDFromBytes(source.getBytes(StandardCharsets.UTF_8))); } - private record JoinableRoomListApiResponse( - String resultType, - List success, - Object error - ) { - } - private static HttpHeaders actionHeaders(String actionToken) { HttpHeaders headers = jsonHeaders(); headers.set("X-Room-Action-Token", actionToken); diff --git a/src/main/java/com/naminhyeok/fantazzk/room/application/AuctionSettlementRunner.java b/src/main/java/com/naminhyeok/fantazzk/room/application/AuctionSettlementRunner.java new file mode 100644 index 00000000..c8e4aaf7 --- /dev/null +++ b/src/main/java/com/naminhyeok/fantazzk/room/application/AuctionSettlementRunner.java @@ -0,0 +1,7 @@ +package com.naminhyeok.fantazzk.room.application; + +import com.naminhyeok.fantazzk.room.domain.Room; + +public interface AuctionSettlementRunner { + Room settleIfDue(String code); +} diff --git a/src/main/java/com/naminhyeok/fantazzk/room/application/CreateRoom.java b/src/main/java/com/naminhyeok/fantazzk/room/application/CreateRoom.java index 5a1b0611..1c4a18ea 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/application/CreateRoom.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/application/CreateRoom.java @@ -37,9 +37,10 @@ public RoomSessionResult create(UUID templateId, String hostNickname) { for (int attempt = 1; attempt <= MAX_ROOM_CODE_ATTEMPTS; attempt++) { Room room = newRoom(template, hostLeaderId, hostActionToken, hostNickname); + RoomTeamLeader hostLeader = room.getLeaders().getFirst(); try { Room saved = createRoomAttempt.save(room); - return new RoomSessionResult(saved, findLeader(saved, hostLeaderId)); + return new RoomSessionResult(saved, hostLeader); } catch (DataIntegrityViolationException ex) { if (!isRoomCodeCollision(ex)) { throw ex; @@ -78,13 +79,6 @@ private Room newRoom( ); } - private RoomTeamLeader findLeader(Room room, TeamLeaderId leaderId) { - return room.getLeaders().stream() - .filter(leader -> leader.getId().equals(leaderId)) - .findFirst() - .orElseThrow(); - } - private String generateCode() { return roomCodeGenerator.generate(); } diff --git a/src/main/java/com/naminhyeok/fantazzk/room/application/JoinRoom.java b/src/main/java/com/naminhyeok/fantazzk/room/application/JoinRoom.java index 26351a9b..8b679811 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/application/JoinRoom.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/application/JoinRoom.java @@ -23,19 +23,12 @@ public RoomSessionResult join(String code, String nickname) { try { Room room = rooms.findByCode(code).orElseThrow(() -> CoreException.of(RoomErrorType.ROOM_NOT_FOUND)); TeamLeaderId joinedLeaderId = new TeamLeaderId(UUID.randomUUID().toString()); - room.join(joinedLeaderId, nickname, UUID.randomUUID().toString()); + RoomTeamLeader joinedLeader = room.join(joinedLeaderId, nickname, UUID.randomUUID().toString()); Room saved = rooms.saveAndFlush(room); realtimeEventPublisher.publishRoomUpdatedAfterCommit(saved); - return new RoomSessionResult(saved, findLeader(saved, joinedLeaderId)); + return new RoomSessionResult(saved, joinedLeader); } catch (OptimisticLockingFailureException ex) { throw CoreException.of(RoomErrorType.ROOM_CONCURRENT_MODIFICATION); } } - - private RoomTeamLeader findLeader(Room room, TeamLeaderId leaderId) { - return room.getLeaders().stream() - .filter(leader -> leader.getId().equals(leaderId)) - .findFirst() - .orElseThrow(); - } } diff --git a/src/main/java/com/naminhyeok/fantazzk/room/application/SettleAuction.java b/src/main/java/com/naminhyeok/fantazzk/room/application/SettleAuction.java index b7ebdff5..70bf8e69 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/application/SettleAuction.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/application/SettleAuction.java @@ -10,7 +10,7 @@ @Service @RequiredArgsConstructor -public class SettleAuction { +public class SettleAuction implements AuctionSettlementRunner { private final SettleAuctionAttempt settleAuctionAttempt; private final Rooms rooms; diff --git a/src/main/java/com/naminhyeok/fantazzk/room/application/SettleAuctionAttempt.java b/src/main/java/com/naminhyeok/fantazzk/room/application/SettleAuctionAttempt.java index f2ad0c50..f3ca05b2 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/application/SettleAuctionAttempt.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/application/SettleAuctionAttempt.java @@ -61,20 +61,6 @@ private AuctionGame loadAuctionGame(Room room) { return auctionGame; } - private AuctionGame requireAuctionGame(Room room) { - if (room.getMode() != RoomMode.AUCTION) { - throw CoreException.of(RoomErrorType.ROOM_BID_REQUIRES_AUCTION_MODE); - } - if (room.getStatus() != RoomStatus.STARTED) { - throw CoreException.of(RoomErrorType.ROOM_PLAY_REQUIRES_IN_PROGRESS); - } - AuctionGame game = loadAuctionGame(room); - if (game == null) { - throw RoomStateInvalidException.auctionRoundMissing(); - } - return game; - } - private static boolean isDue(AuctionGame game, Instant now) { return game != null && game.isDue(now); } diff --git a/src/main/java/com/naminhyeok/fantazzk/room/domain/Room.java b/src/main/java/com/naminhyeok/fantazzk/room/domain/Room.java index ae4f0ad3..b9111db1 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/domain/Room.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/domain/Room.java @@ -16,8 +16,8 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import java.util.Objects; import java.util.Locale; +import java.util.Objects; import java.util.UUID; import lombok.Getter; import org.jmolecules.ddd.types.AggregateRoot; @@ -150,7 +150,7 @@ public boolean isJoinable() { return status == RoomStatus.WAITING && leaders.size() < teamCount; } - public void join(TeamLeaderId teamLeaderId, String nickname, String actionToken) { + public RoomTeamLeader join(TeamLeaderId teamLeaderId, String nickname, String actionToken) { if (status != RoomStatus.WAITING) { throw CoreException.of(RoomErrorType.ROOM_JOIN_REQUIRES_WAITING); } @@ -168,7 +168,9 @@ public void join(TeamLeaderId teamLeaderId, String nickname, String actionToken) throw CoreException.of(RoomErrorType.ROOM_NICKNAME_ALREADY_TAKEN); } - leaders.add(new RoomTeamLeader(teamLeaderId, nickname.trim(), actionToken, budget)); + RoomTeamLeader joinedLeader = new RoomTeamLeader(teamLeaderId, nickname.trim(), actionToken, budget); + leaders.add(joinedLeader); + return joinedLeader; } private String normalizeNickname(String nickname) { diff --git a/src/main/java/com/naminhyeok/fantazzk/room/domain/TeamLeaderRole.java b/src/main/java/com/naminhyeok/fantazzk/room/domain/TeamLeaderRole.java deleted file mode 100644 index a9207bc5..00000000 --- a/src/main/java/com/naminhyeok/fantazzk/room/domain/TeamLeaderRole.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.naminhyeok.fantazzk.room.domain; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "현재 사용자의 방 역할") -public enum TeamLeaderRole { - HOST, - LEADER -} diff --git a/src/main/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/AuctionScheduleJpaRepository.java b/src/main/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/AuctionScheduleJpaRepository.java index 01f66467..74c751c6 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/AuctionScheduleJpaRepository.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/AuctionScheduleJpaRepository.java @@ -7,5 +7,5 @@ import org.springframework.data.repository.Repository; public interface AuctionScheduleJpaRepository extends Repository { - public List findByCurrentRoundEndsAtNotNullOrderByRoomCodeAsc(Pageable pageable); + List findByCurrentRoundEndsAtNotNullOrderByRoomCodeAsc(Pageable pageable); } diff --git a/src/main/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/JoinableRoomJpaRepository.java b/src/main/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/JoinableRoomJpaRepository.java index 96237af7..c1828b26 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/JoinableRoomJpaRepository.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/JoinableRoomJpaRepository.java @@ -10,5 +10,5 @@ public interface JoinableRoomJpaRepository extends Repository { @EntityGraph(attributePaths = "leaders") - public List findByStatusOrderByCreatedAtDescCodeDesc(RoomStatus status, Pageable pageable); + List findByStatusOrderByCreatedAtDescCodeDesc(RoomStatus status, Pageable pageable); } diff --git a/src/main/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/JpaAuctionScheduleReader.java b/src/main/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/JpaAuctionScheduleReader.java index 8c97e7e1..e2f194ee 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/JpaAuctionScheduleReader.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/JpaAuctionScheduleReader.java @@ -2,6 +2,7 @@ import com.naminhyeok.fantazzk.room.domain.AuctionGame; import com.naminhyeok.fantazzk.room.query.AuctionScheduleCandidate; +import com.naminhyeok.fantazzk.room.query.AuctionScheduleReader; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; @@ -10,11 +11,12 @@ @Repository @RequiredArgsConstructor -public class JpaAuctionScheduleReader { +public class JpaAuctionScheduleReader implements AuctionScheduleReader { private static final int CATCH_UP_BATCH_SIZE = 200; private final AuctionScheduleJpaRepository repository; + @Override public List findInProgressAuctionSchedules() { List candidates = new ArrayList<>(); int page = 0; diff --git a/src/main/java/com/naminhyeok/fantazzk/room/infrastructure/schedule/RoomAuctionDeadlineScheduler.java b/src/main/java/com/naminhyeok/fantazzk/room/infrastructure/schedule/RoomAuctionDeadlineScheduler.java index 2020fa01..b908e3e4 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/infrastructure/schedule/RoomAuctionDeadlineScheduler.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/infrastructure/schedule/RoomAuctionDeadlineScheduler.java @@ -1,13 +1,13 @@ package com.naminhyeok.fantazzk.room.infrastructure.schedule; -import com.naminhyeok.fantazzk.room.application.SettleAuction; -import com.naminhyeok.fantazzk.room.infrastructure.persistence.JpaAuctionScheduleReader; +import com.naminhyeok.fantazzk.room.application.AuctionSettlementRunner; import com.naminhyeok.fantazzk.room.domain.AuctionGame; import com.naminhyeok.fantazzk.room.domain.Room; import com.naminhyeok.fantazzk.room.domain.RoomMode; import com.naminhyeok.fantazzk.room.domain.RoomStateInvalidException; import com.naminhyeok.fantazzk.room.domain.RoomStatus; import com.naminhyeok.fantazzk.room.query.AuctionScheduleCandidate; +import com.naminhyeok.fantazzk.room.query.AuctionScheduleReader; import com.naminhyeok.fantazzk.room.repository.Games; import java.time.Clock; import java.time.Instant; @@ -26,17 +26,17 @@ @RequiredArgsConstructor public class RoomAuctionDeadlineScheduler { private final TaskScheduler taskScheduler; - private final SettleAuction settleAuction; - private final JpaAuctionScheduleReader auctionScheduleReader; + private final AuctionSettlementRunner settleAuction; + private final AuctionScheduleReader auctionScheduleReader; private final Clock clock; private final Games games; private final ConcurrentMap> scheduledTasks = new ConcurrentHashMap<>(); - public void schedule(Room room) { + void schedule(Room room) { refresh(room.getCode(), resolveDeadline(room)); } - public void refresh(String code, Instant deadline) { + void refresh(String code, Instant deadline) { cancel(code); if (deadline == null) { return; @@ -51,7 +51,7 @@ private void schedule(String code, Instant deadline) { } } - public void cancel(String code) { + void cancel(String code) { ScheduledFuture future = scheduledTasks.remove(code); if (future != null) { future.cancel(false); diff --git a/src/main/java/com/naminhyeok/fantazzk/room/query/AuctionScheduleReader.java b/src/main/java/com/naminhyeok/fantazzk/room/query/AuctionScheduleReader.java new file mode 100644 index 00000000..3df3b045 --- /dev/null +++ b/src/main/java/com/naminhyeok/fantazzk/room/query/AuctionScheduleReader.java @@ -0,0 +1,7 @@ +package com.naminhyeok.fantazzk.room.query; + +import java.util.List; + +public interface AuctionScheduleReader { + List findInProgressAuctionSchedules(); +} diff --git a/src/main/java/com/naminhyeok/fantazzk/room/query/AuctionTargetResponse.java b/src/main/java/com/naminhyeok/fantazzk/room/query/AuctionTargetResponse.java index ba1689b4..13ddc883 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/query/AuctionTargetResponse.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/query/AuctionTargetResponse.java @@ -1,7 +1,6 @@ package com.naminhyeok.fantazzk.room.query; import com.naminhyeok.fantazzk.room.domain.GamePlayer; -import com.naminhyeok.fantazzk.room.domain.RoomPlayer; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "현재 경매 대상 선수") @@ -11,13 +10,6 @@ public record AuctionTargetResponse( @Schema(description = "FE가 관리하는 선수 포지션 메타데이터", example = "JUNGLE", nullable = true) String position ) { - public static AuctionTargetResponse from(RoomPlayer player) { - if (player == null) { - return null; - } - return new AuctionTargetResponse(player.getName(), player.getPosition()); - } - public static AuctionTargetResponse from(GamePlayer player) { if (player == null) { return null; diff --git a/src/main/java/com/naminhyeok/fantazzk/room/query/JoinableRoomReader.java b/src/main/java/com/naminhyeok/fantazzk/room/query/JoinableRoomReader.java index 5dd71e77..1d82e9eb 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/query/JoinableRoomReader.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/query/JoinableRoomReader.java @@ -4,5 +4,5 @@ import java.util.List; public interface JoinableRoomReader { - public List findLatestWaitingRooms(int limit); + List findLatestWaitingRooms(int limit); } diff --git a/src/main/java/com/naminhyeok/fantazzk/room/query/RoomPlayerResponse.java b/src/main/java/com/naminhyeok/fantazzk/room/query/RoomPlayerResponse.java index 978a239b..558da468 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/query/RoomPlayerResponse.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/query/RoomPlayerResponse.java @@ -1,6 +1,5 @@ package com.naminhyeok.fantazzk.room.query; -import com.naminhyeok.fantazzk.room.domain.PlayerStatus; import com.naminhyeok.fantazzk.room.domain.RoomPlayer; import io.swagger.v3.oas.annotations.media.Schema; @@ -24,12 +23,4 @@ public static RoomPlayerResponse from(RoomPlayer player) { ); } - public static RoomPlayerResponse from(RoomPlayer player, int displayOrder, boolean assigned) { - return new RoomPlayerResponse( - player.getName(), - player.getPosition(), - displayOrder, - assigned ? PlayerStatus.ASSIGNED.name() : PlayerStatus.AVAILABLE.name() - ); - } } diff --git a/src/main/java/com/naminhyeok/fantazzk/room/web/TeamLeaderSessionResponse.java b/src/main/java/com/naminhyeok/fantazzk/room/web/TeamLeaderSessionResponse.java index 1ce6787f..bac500ad 100644 --- a/src/main/java/com/naminhyeok/fantazzk/room/web/TeamLeaderSessionResponse.java +++ b/src/main/java/com/naminhyeok/fantazzk/room/web/TeamLeaderSessionResponse.java @@ -2,7 +2,6 @@ import com.naminhyeok.fantazzk.room.domain.Room; import com.naminhyeok.fantazzk.room.domain.RoomTeamLeader; -import com.naminhyeok.fantazzk.room.domain.TeamLeaderRole; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "현재 사용자에게 발급된 방 세션 정보") @@ -10,7 +9,7 @@ public record TeamLeaderSessionResponse( @Schema(description = "현재 사용자 팀장 ID", example = "leader-host") String leaderId, @Schema(description = "현재 사용자 역할. HOST 만 방 시작 권한을 가집니다.", example = "HOST") - TeamLeaderRole role, + Role role, @Schema( description = "이후 모든 mutation 요청의 `X-Room-Action-Token` 헤더에 넣어야 하는 값입니다. " + "로그인 토큰이 아니라 방 액션 전용 토큰입니다.", @@ -21,8 +20,13 @@ public record TeamLeaderSessionResponse( public static TeamLeaderSessionResponse from(Room room, RoomTeamLeader leader) { return new TeamLeaderSessionResponse( leader.getId().value(), - room.getHostLeaderId().equals(leader.getId()) ? TeamLeaderRole.HOST : TeamLeaderRole.LEADER, + room.getHostLeaderId().equals(leader.getId()) ? Role.HOST : Role.LEADER, leader.getActionToken() ); } + + public enum Role { + HOST, + LEADER + } } diff --git a/src/test/java/com/naminhyeok/fantazzk/room/application/CreateRoomTest.java b/src/test/java/com/naminhyeok/fantazzk/room/application/CreateRoomTest.java index 575a944c..af577581 100644 --- a/src/test/java/com/naminhyeok/fantazzk/room/application/CreateRoomTest.java +++ b/src/test/java/com/naminhyeok/fantazzk/room/application/CreateRoomTest.java @@ -11,9 +11,6 @@ import com.naminhyeok.fantazzk.room.domain.Room; import com.naminhyeok.fantazzk.room.domain.RoomErrorType; import com.naminhyeok.fantazzk.room.domain.RoomId; -import com.naminhyeok.fantazzk.room.domain.RoomPlayer; -import com.naminhyeok.fantazzk.room.domain.RoomPlayerId; -import com.naminhyeok.fantazzk.room.domain.TeamLeaderId; import com.naminhyeok.fantazzk.room.repository.Rooms; import com.naminhyeok.fantazzk.template.TemplateCatalog; import java.time.Clock; @@ -50,10 +47,6 @@ class CreateRoomTest { assertThat(created.room()).isSameAs(rooms.savedRoom()); assertThat(rooms.flushAttemptCount()).isEqualTo(3); assertThat(rooms.attemptedRoomIds()).doesNotHaveDuplicates(); - assertThat(created.room().getPickBanTime()).isEqualTo(45); - assertThat(created.room().getMinBidUnit()).isEqualTo(10); - assertThat(created.room().getPlayers().stream().map(RoomPlayer::getId)) - .containsExactly(new RoomPlayerId(0), new RoomPlayerId(1)); assertThat(created.room().getLeaders()).singleElement().extracting(leader -> leader.getId()).isEqualTo(created.leader().getId()); assertThat(created.leader().getActionToken()).isNotBlank(); } diff --git a/src/test/java/com/naminhyeok/fantazzk/room/application/JoinRoomTest.java b/src/test/java/com/naminhyeok/fantazzk/room/application/JoinRoomTest.java deleted file mode 100644 index 510f905b..00000000 --- a/src/test/java/com/naminhyeok/fantazzk/room/application/JoinRoomTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.naminhyeok.fantazzk.room.application; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.naminhyeok.fantazzk.room.application.JoinRoom; -import com.naminhyeok.fantazzk.room.application.RoomSessionResult; -import com.naminhyeok.fantazzk.room.domain.Room; -import com.naminhyeok.fantazzk.room.domain.RoomId; -import com.naminhyeok.fantazzk.room.domain.RoomTeamLeader; -import com.naminhyeok.fantazzk.room.infrastructure.realtime.NoopRoomRealtimeEventPublisher; -import com.naminhyeok.fantazzk.room.repository.Rooms; -import java.lang.reflect.Field; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.Test; -import com.naminhyeok.fantazzk.room.support.RoomApiTestFixtures; - -class JoinRoomTest { - @Test - void 저장후_팀장_순서가_바뀌어도_새로_참가한_팀장_세션을_반환한다() { - Room room = RoomApiTestFixtures.waitingAuctionRoom(); - JoinRoom joinRoom = - new JoinRoom( - new ReorderingRooms(room), - new NoopRoomRealtimeEventPublisher() - ); - - RoomSessionResult joined = joinRoom.join(room.getCode(), "게스트"); - - assertThat(joined.leader().getActionToken()).isNotBlank(); - assertThat(joined.room().getLeaders()) - .extracting(RoomTeamLeader::getId) - .containsExactly(joined.leader().getId(), room.getLeaders().getLast().getId()); - } - - private static final class ReorderingRooms implements Rooms { - private final Room room; - - private ReorderingRooms(Room room) { - this.room = room; - } - - @Override - public Room save(Room room) { - return room; - } - - @Override - public Room saveAndFlush(Room room) { - reverseLeaderOrder(room); - return room; - } - - @Override - public Optional findById(RoomId id) { - return Optional.of(room).filter(saved -> saved.getId().equals(id)); - } - - @Override - public Optional findByCode(String code) { - return Optional.of(room).filter(saved -> saved.getCode().equals(code)); - } - - @SuppressWarnings("unchecked") - private static void reverseLeaderOrder(Room room) { - try { - Field field = Room.class.getDeclaredField("leaders"); - field.setAccessible(true); - Collections.reverse((List) field.get(room)); - } catch (ReflectiveOperationException ex) { - throw new AssertionError(ex); - } - } - } -} diff --git a/src/test/java/com/naminhyeok/fantazzk/room/application/RoomRealtimePublishingTest.java b/src/test/java/com/naminhyeok/fantazzk/room/application/RoomRealtimePublishingTest.java index 636e151d..9b2a76dc 100644 --- a/src/test/java/com/naminhyeok/fantazzk/room/application/RoomRealtimePublishingTest.java +++ b/src/test/java/com/naminhyeok/fantazzk/room/application/RoomRealtimePublishingTest.java @@ -6,18 +6,11 @@ import com.naminhyeok.fantazzk.CoreException; import com.naminhyeok.fantazzk.room.application.ClearDraftPosition; import com.naminhyeok.fantazzk.room.application.JoinRoom; -import com.naminhyeok.fantazzk.room.application.PickDraft; -import com.naminhyeok.fantazzk.room.application.PlaceBid; import com.naminhyeok.fantazzk.room.application.RoomActionAuthorizer; import com.naminhyeok.fantazzk.room.application.RoomRealtimeEventPublisher; -import com.naminhyeok.fantazzk.room.application.RoomSessionResult; import com.naminhyeok.fantazzk.room.application.SelectDraftPosition; import com.naminhyeok.fantazzk.room.application.SettleAuctionAttempt; -import com.naminhyeok.fantazzk.room.application.StartRoom; -import com.naminhyeok.fantazzk.room.application.StartedGameContextLoader; -import com.naminhyeok.fantazzk.room.domain.AuctionBid; import com.naminhyeok.fantazzk.room.domain.AuctionGame; -import com.naminhyeok.fantazzk.room.domain.DraftGame; import com.naminhyeok.fantazzk.room.domain.DraftOrderStrategy; import com.naminhyeok.fantazzk.room.domain.Game; import com.naminhyeok.fantazzk.room.domain.GameFactory; @@ -30,13 +23,10 @@ import com.naminhyeok.fantazzk.room.domain.RoomId; import com.naminhyeok.fantazzk.room.domain.RoomMode; import com.naminhyeok.fantazzk.room.domain.RoomPlayerId; -import com.naminhyeok.fantazzk.room.domain.RoomStatus; import com.naminhyeok.fantazzk.room.domain.RoomTemplateSpec; -import com.naminhyeok.fantazzk.room.domain.RosterMember; import com.naminhyeok.fantazzk.room.domain.StartedGameSnapshot; import com.naminhyeok.fantazzk.room.domain.StartedRoomSnapshot; import com.naminhyeok.fantazzk.room.domain.TeamLeaderId; -import com.naminhyeok.fantazzk.room.infrastructure.realtime.GameUpdatedEvent; import com.naminhyeok.fantazzk.room.infrastructure.realtime.RoomRealtimeEvent; import com.naminhyeok.fantazzk.room.infrastructure.realtime.RoomRealtimeEventFactory; import com.naminhyeok.fantazzk.room.infrastructure.realtime.RoomUpdatedEvent; @@ -60,22 +50,6 @@ class RoomRealtimePublishingTest { private static final String GUEST_ID = "leader-guest"; private static final String GUEST_ACTION_TOKEN = "guest-token"; - @Test - void 방_참가는_로비_스냅샷을_발행한다() { - Room room = waitingAuctionRoom(); - RecordingRoomRealtimeEventPublisher publisher = new RecordingRoomRealtimeEventPublisher(); - JoinRoom joinRoom = new JoinRoom(new SaveAndFlushOnlyRooms(room), publisher); - - RoomSessionResult joined = joinRoom.join(room.getCode(), "게스트"); - - assertThat(joined.leader().getId()).isEqualTo(joined.room().getLeaders().getLast().getId()); - assertThat(publisher.events).hasSize(1); - assertThat(publisher.events.getFirst()).isInstanceOf(RoomUpdatedEvent.class); - RoomUpdatedEvent event = (RoomUpdatedEvent) publisher.events.getFirst(); - assertThat(event.roomCode()).isEqualTo(room.getCode()); - assertThat(event.room().leaders()).hasSize(2); - } - @Test void 방_참가_저장_충돌은_동시_수정_오류로_번역하고_이벤트를_발행하지_않는다() { Room room = waitingAuctionRoom(); @@ -87,76 +61,6 @@ class RoomRealtimePublishingTest { assertThat(publisher.events).isEmpty(); } - @Test - void 방_시작은_로비와_게임_스냅샷을_함께_발행한다() { - Room room = joinedAuctionRoom(); - RecordingRoomRealtimeEventPublisher publisher = new RecordingRoomRealtimeEventPublisher(); - StartRoom startRoom = new StartRoom( - new InMemoryRooms(room), - new InMemoryGames(null), - new RoomActionAuthorizer(), - new GameFactory(), - publisher, - Clock.fixed(CREATED_AT, ZoneOffset.UTC) - ); - - Game started = startRoom.start(room.getCode(), HOST_ACTION_TOKEN); - - assertThat(started.getRoomCode()).isEqualTo(room.getCode()); - assertThat(publisher.events).hasSize(2); - assertThat(publisher.events.get(0)).isInstanceOf(RoomUpdatedEvent.class); - assertThat(publisher.events.get(1)).isInstanceOf(GameUpdatedEvent.class); - RoomUpdatedEvent roomUpdated = (RoomUpdatedEvent) publisher.events.get(0); - GameUpdatedEvent event = (GameUpdatedEvent) publisher.events.get(1); - assertThat(roomUpdated.room().startedGameId()).isEqualTo(started.getId().gameId().toString()); - assertThat(event.game().gameId()).isEqualTo(started.getId().gameId().toString()); - } - - @Test - void 입찰은_게임_스냅샷을_발행한다() { - Room room = startedAuctionRoom(); - AuctionGame game = (AuctionGame) gameFor(room); - RecordingRoomRealtimeEventPublisher publisher = new RecordingRoomRealtimeEventPublisher(); - InMemoryRooms rooms = new InMemoryRooms(room); - InMemoryGames games = new InMemoryGames(game); - PlaceBid placeBid = new PlaceBid( - rooms, - games, - startedGameContextLoader(rooms, games), - publisher, - Clock.fixed(CREATED_AT.plusSeconds(1), ZoneOffset.UTC) - ); - - AuctionBid bid = placeBid.place(game.getId().gameId(), HOST_ACTION_TOKEN, 100); - - assertThat(bid.amount()).isEqualTo(100); - assertThat(publisher.events).hasSize(1); - assertThat(publisher.events.getFirst()).isInstanceOf(GameUpdatedEvent.class); - GameUpdatedEvent event = (GameUpdatedEvent) publisher.events.getFirst(); - assertThat(event.game().gameId()).isEqualTo(game.getId().gameId().toString()); - assertThat(event.game().auctionProgress().highestBidAmount()).isEqualTo(100); - } - - @Test - void 드래프트_픽은_게임_스냅샷을_발행한다() { - Room room = startedDraftRoom(); - DraftGame game = (DraftGame) gameFor(room); - RecordingRoomRealtimeEventPublisher publisher = new RecordingRoomRealtimeEventPublisher(); - InMemoryRooms rooms = new InMemoryRooms(room); - InMemoryGames games = new InMemoryGames(game); - PickDraft pickDraft = - new PickDraft(rooms, games, startedGameContextLoader(rooms, games), publisher); - - RosterMember picked = pickDraft.pick(game.getId().gameId(), HOST_ACTION_TOKEN, "선수1"); - - assertThat(picked.playerName()).isEqualTo("선수1"); - assertThat(publisher.events).hasSize(1); - assertThat(publisher.events.getFirst()).isInstanceOf(GameUpdatedEvent.class); - GameUpdatedEvent event = (GameUpdatedEvent) publisher.events.getFirst(); - assertThat(event.game().roster()).hasSize(1); - assertThat(event.game().draftProgress().currentTurnIndex()).isEqualTo(1); - } - @Test void 드래프트_자리_선택은_로비_스냅샷을_발행한다() { Room room = waitingDraftRoomForPositionChange(); @@ -189,52 +93,6 @@ class RoomRealtimePublishingTest { assertThat(event.room().draftOrder().slots().getFirst().leaderId()).isNull(); } - @Test - void 유찰_정산은_게임_스냅샷만_발행한다() { - Room room = startedAuctionRoom(); - AuctionGame game = (AuctionGame) gameFor(room); - RecordingRoomRealtimeEventPublisher publisher = new RecordingRoomRealtimeEventPublisher(); - InMemoryRooms rooms = new InMemoryRooms(room); - InMemoryGames games = new InMemoryGames(game); - SettleAuctionAttempt settleAuctionAttempt = - new SettleAuctionAttempt( - rooms, - games, - Clock.fixed(CREATED_AT.plusSeconds(40), ZoneOffset.UTC), - publisher - ); - - Room settled = settleAuctionAttempt.settleIfDue(room.getCode()); - - assertThat(settled.getStatus()).isEqualTo(RoomStatus.STARTED); - assertThat(publisher.events).hasSize(1); - assertThat(publisher.events.getFirst()).isInstanceOf(GameUpdatedEvent.class); - } - - @Test - void 낙찰_정산은_게임_스냅샷을_발행한다() { - Room room = startedAuctionRoom(); - AuctionGame game = (AuctionGame) gameFor(room); - game.placeBid(new TeamLeaderId(HOST_ID), 100, CREATED_AT.plusSeconds(1)); - RecordingRoomRealtimeEventPublisher publisher = new RecordingRoomRealtimeEventPublisher(); - InMemoryRooms rooms = new InMemoryRooms(room); - InMemoryGames games = new InMemoryGames(game); - SettleAuctionAttempt settleAuctionAttempt = - new SettleAuctionAttempt( - rooms, - games, - Clock.fixed(CREATED_AT.plusSeconds(40), ZoneOffset.UTC), - publisher - ); - - Room settled = settleAuctionAttempt.settleIfDue(room.getCode()); - - assertThat(settled.getStatus()).isEqualTo(RoomStatus.STARTED); - assertThat(game.getMembers()).singleElement().extracting(RosterMember::playerName).isEqualTo("선수1"); - assertThat(publisher.events).hasSize(1); - assertThat(publisher.events.getFirst()).isInstanceOf(GameUpdatedEvent.class); - } - @Test void 마감_전_정산은_스냅샷을_발행하지_않는다() { Room room = startedAuctionRoom(); @@ -250,16 +108,11 @@ class RoomRealtimePublishingTest { publisher ); - Room current = settleAuctionAttempt.settleIfDue(room.getCode()); + settleAuctionAttempt.settleIfDue(room.getCode()); - assertThat(current.getStatus()).isEqualTo(RoomStatus.STARTED); assertThat(publisher.events).isEmpty(); } - private static StartedGameContextLoader startedGameContextLoader(InMemoryRooms rooms, InMemoryGames games) { - return new StartedGameContextLoader(rooms, games, new RoomActionAuthorizer()); - } - private static void assertRoomError(CoreException exception, RoomErrorType errorType) { assertThat(exception.getError()).isSameAs(errorType); } @@ -300,14 +153,6 @@ private static Room startedAuctionRoom() { return room; } - private static Room startedDraftRoom() { - Room room = waitingDraftRoomForPositionChange(); - room.selectDraftPosition(new TeamLeaderId(HOST_ID), 1); - room.selectDraftPosition(new TeamLeaderId(GUEST_ID), 2); - room.start(new TeamLeaderId(HOST_ID), deterministicGameId(room), CREATED_AT); - return room; - } - private static Room waitingDraftRoomForPositionChange() { Room room = Room.createFromTemplate( @@ -389,34 +234,6 @@ public void publishGameUpdatedAfterCommit(StartedRoomSnapshot snapshot) { } } - private static final class SaveAndFlushOnlyRooms implements Rooms { - private final Room room; - - private SaveAndFlushOnlyRooms(Room room) { - this.room = room; - } - - @Override - public Room save(Room room) { - throw new UnsupportedOperationException("JoinRoom should use saveAndFlush"); - } - - @Override - public Room saveAndFlush(Room room) { - return room; - } - - @Override - public Optional findById(RoomId id) { - return Optional.ofNullable(room).filter(it -> it.getId().equals(id)); - } - - @Override - public Optional findByCode(String code) { - return Optional.ofNullable(room).filter(it -> it.getCode().equals(code)); - } - } - private static final class InMemoryRooms implements Rooms { private Room room; diff --git a/src/test/java/com/naminhyeok/fantazzk/room/domain/RoomAggregateTest.java b/src/test/java/com/naminhyeok/fantazzk/room/domain/RoomAggregateTest.java index b2492ad4..4fba4a54 100644 --- a/src/test/java/com/naminhyeok/fantazzk/room/domain/RoomAggregateTest.java +++ b/src/test/java/com/naminhyeok/fantazzk/room/domain/RoomAggregateTest.java @@ -97,11 +97,13 @@ class RoomAggregateTest { void 참가하면_팀장을_추가한다() { Room room = auctionWaitingRoom(); - room.join(new TeamLeaderId(GUEST_ID), " 게스트 ", GUEST_ACTION_TOKEN); + RoomTeamLeader joinedLeader = room.join(new TeamLeaderId(GUEST_ID), " 게스트 ", GUEST_ACTION_TOKEN); assertThat(room.getLeaders()).hasSize(2); - assertThat(room.getLeaders().getLast().getNickname()).isEqualTo("게스트"); - assertThat(room.getLeaders().getLast().getActionToken()).isEqualTo(GUEST_ACTION_TOKEN); + assertThat(joinedLeader.getId()).isEqualTo(new TeamLeaderId(GUEST_ID)); + assertThat(joinedLeader.getNickname()).isEqualTo("게스트"); + assertThat(joinedLeader.getActionToken()).isEqualTo(GUEST_ACTION_TOKEN); + assertThat(room.getLeaders().getLast()).isSameAs(joinedLeader); } @Test diff --git a/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/JpaAuctionScheduleReaderTest.java b/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/JpaAuctionScheduleReaderTest.java new file mode 100644 index 00000000..7f11502c --- /dev/null +++ b/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/persistence/JpaAuctionScheduleReaderTest.java @@ -0,0 +1,73 @@ +package com.naminhyeok.fantazzk.room.infrastructure.persistence; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.naminhyeok.fantazzk.room.domain.AuctionGame; +import com.naminhyeok.fantazzk.room.domain.GameId; +import com.naminhyeok.fantazzk.room.domain.GameRules; +import com.naminhyeok.fantazzk.room.domain.RoomId; +import com.naminhyeok.fantazzk.room.query.AuctionScheduleCandidate; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Pageable; + +class JpaAuctionScheduleReaderTest { + @Test + void 경매_마감_후보를_모든_페이지에서_읽는다() { + JpaAuctionScheduleReader reader = new JpaAuctionScheduleReader(new RecordingScheduleRepository(205)); + + List candidates = reader.findInProgressAuctionSchedules(); + + assertThat(candidates).hasSize(205); + assertThat(candidates).extracting(AuctionScheduleCandidate::code) + .contains("ROOM000", "ROOM204"); + } + + private static final class RecordingScheduleRepository implements AuctionScheduleJpaRepository { + private final List games; + + private RecordingScheduleRepository(int count) { + this.games = new ArrayList<>(); + for (int index = 0; index < count; index++) { + games.add(auctionGame("ROOM%03d".formatted(index), Instant.parse("2026-04-09T00:00:00Z").plusSeconds(index))); + } + } + + @Override + public List findByCurrentRoundEndsAtNotNullOrderByRoomCodeAsc(Pageable pageable) { + int fromIndex = (int) pageable.getOffset(); + if (fromIndex >= games.size()) { + return List.of(); + } + int toIndex = Math.min(fromIndex + pageable.getPageSize(), games.size()); + return games.subList(fromIndex, toIndex); + } + } + + private static AuctionGame auctionGame(String code, Instant deadline) { + return new AuctionGame( + deterministicGameId("game:" + code), + new RoomId(deterministicUuid("room:" + code)), + code, + "LEAGUE_OF_LEGENDS", + Instant.parse("2026-04-09T00:00:00Z"), + GameRules.auction(2, 2, 300, 30, 10), + List.of(), + List.of(), + 1, + deadline + ); + } + + private static GameId deterministicGameId(String source) { + return new GameId(deterministicUuid(source)); + } + + private static UUID deterministicUuid(String source) { + return UUID.nameUUIDFromBytes(source.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/realtime/RoomRealtimeEventFactoryTest.java b/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/realtime/RoomRealtimeEventFactoryTest.java index 77ce5d79..f3baeeb8 100644 --- a/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/realtime/RoomRealtimeEventFactoryTest.java +++ b/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/realtime/RoomRealtimeEventFactoryTest.java @@ -63,18 +63,6 @@ class RoomRealtimeEventFactoryTest { assertThat(gameUpdated.game().auctionProgress().currentRound()).isEqualTo(1); } - @Test - void 게임_갱신_스냅샷_버전은_방과_게임_버전을_합산한다() throws Exception { - Room room = startedAuctionRoom(); - Game game = startedGameOf(room); - setVersion(Room.class, room, 1L); - setVersion(Game.class, game, 7L); - - RoomRealtimeEvent event = RoomRealtimeEventFactory.gameUpdated(new StartedRoomSnapshot(room, game), PUBLISHED_AT); - - assertThat(event.snapshotVersion()).isEqualTo(8L); - } - private Room waitingAuctionRoom() { return Room.createFromTemplate( "AUC002", @@ -127,12 +115,6 @@ private Game startedGameOf(Room room) { return new GameFactory().create(snapshot); } - private static void setVersion(Class owner, Object target, long version) throws Exception { - var field = owner.getDeclaredField("version"); - field.setAccessible(true); - field.setLong(target, version); - } - private static com.naminhyeok.fantazzk.room.domain.GameId deterministicGameId(Room room) { String source = "game:%s".formatted(room.getId().roomId()); return new com.naminhyeok.fantazzk.room.domain.GameId( diff --git a/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/realtime/SupabaseRoomRealtimePublisherTest.java b/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/realtime/SupabaseRoomRealtimePublisherTest.java index 90bffcd6..38f2313b 100644 --- a/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/realtime/SupabaseRoomRealtimePublisherTest.java +++ b/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/realtime/SupabaseRoomRealtimePublisherTest.java @@ -85,8 +85,6 @@ class SupabaseRoomRealtimePublisherTest { publisher.publishRoomUpdatedAfterCommit(room); List synchronizations = TransactionSynchronizationManager.getSynchronizations(); - assertThat(synchronizations).hasSize(1); - TransactionSynchronizationManager.clearSynchronization(); synchronizations.forEach(TransactionSynchronization::afterCommit); } finally { @@ -124,8 +122,6 @@ class SupabaseRoomRealtimePublisherTest { clock.advanceSeconds(30); List synchronizations = TransactionSynchronizationManager.getSynchronizations(); - assertThat(synchronizations).hasSize(1); - TransactionSynchronizationManager.clearSynchronization(); synchronizations.forEach(TransactionSynchronization::afterCommit); } finally { diff --git a/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/schedule/RoomAuctionDeadlineSchedulerTest.java b/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/schedule/RoomAuctionDeadlineSchedulerTest.java index 490153fb..1fb69853 100644 --- a/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/schedule/RoomAuctionDeadlineSchedulerTest.java +++ b/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/schedule/RoomAuctionDeadlineSchedulerTest.java @@ -1,35 +1,32 @@ package com.naminhyeok.fantazzk.room.infrastructure.schedule; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import com.naminhyeok.fantazzk.room.application.SettleAuction; +import com.naminhyeok.fantazzk.room.application.AuctionSettlementRunner; import com.naminhyeok.fantazzk.room.domain.AuctionGame; +import com.naminhyeok.fantazzk.room.domain.AuctionParticipant; import com.naminhyeok.fantazzk.room.domain.Game; -import com.naminhyeok.fantazzk.room.domain.GameFactory; import com.naminhyeok.fantazzk.room.domain.GameId; +import com.naminhyeok.fantazzk.room.domain.GamePlayer; +import com.naminhyeok.fantazzk.room.domain.GameRules; import com.naminhyeok.fantazzk.room.domain.Room; import com.naminhyeok.fantazzk.room.domain.RoomMode; import com.naminhyeok.fantazzk.room.domain.RoomPlayerId; import com.naminhyeok.fantazzk.room.domain.RoomStateInvalidException; import com.naminhyeok.fantazzk.room.domain.RoomTemplateSpec; -import com.naminhyeok.fantazzk.room.domain.StartedGameSnapshot; import com.naminhyeok.fantazzk.room.domain.TeamLeaderId; -import com.naminhyeok.fantazzk.room.infrastructure.persistence.AuctionScheduleJpaRepository; -import com.naminhyeok.fantazzk.room.infrastructure.persistence.JpaAuctionScheduleReader; -import com.naminhyeok.fantazzk.room.infrastructure.schedule.RoomAuctionDeadlineScheduler; +import com.naminhyeok.fantazzk.room.query.AuctionScheduleCandidate; +import com.naminhyeok.fantazzk.room.query.AuctionScheduleReader; import com.naminhyeok.fantazzk.room.repository.Games; -import org.springframework.data.domain.Pageable; import java.time.Clock; import java.time.Instant; import java.time.ZoneOffset; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.UUID; import org.junit.jupiter.api.Test; -import org.mockito.InOrder; class RoomAuctionDeadlineSchedulerTest { private static final Instant NOW = Instant.parse("2026-04-09T00:00:10Z"); @@ -37,15 +34,15 @@ class RoomAuctionDeadlineSchedulerTest { @Test void 경매_마감_예약은_정산_후_다음_라운드_마감으로_이어진다() { FakeTaskScheduler taskScheduler = new FakeTaskScheduler(); - SettleAuction settleAuction = mock(SettleAuction.class); + RecordingSettleAuction settleAuction = new RecordingSettleAuction(); StartedAuctionContext room = auctionRoomWithDeadline("ROOM01", Instant.parse("2026-04-09T00:00:15Z")); StartedAuctionContext nextRoundRoom = auctionRoomWithDeadline("ROOM01", Instant.parse("2026-04-09T00:00:30Z")); - given(settleAuction.settleIfDue("ROOM01")).willReturn(nextRoundRoom.room()); + settleAuction.returnRoom("ROOM01", nextRoundRoom.room()); RoomAuctionDeadlineScheduler scheduler = new RoomAuctionDeadlineScheduler( taskScheduler, settleAuction, - new JpaAuctionScheduleReader(new RecordingScheduleRepository()), + new RecordingScheduleReader(), Clock.fixed(NOW, ZoneOffset.UTC), new InMemoryGames(room.game(), nextRoundRoom.game()) ); @@ -56,21 +53,21 @@ class RoomAuctionDeadlineSchedulerTest { taskScheduler.runLatest(); - verify(settleAuction).settleIfDue("ROOM01"); + assertThat(settleAuction.settledCodes()).containsExactly("ROOM01"); assertThat(taskScheduler.activeScheduledInstants()).containsExactly(Instant.parse("2026-04-09T00:00:30Z")); } @Test void 같은_방의_마감은_하나만_활성화된다() { FakeTaskScheduler taskScheduler = new FakeTaskScheduler(); - SettleAuction settleAuction = mock(SettleAuction.class); + RecordingSettleAuction settleAuction = new RecordingSettleAuction(); StartedAuctionContext first = auctionRoomWithDeadline("ROOM01", Instant.parse("2026-04-09T00:00:15Z")); StartedAuctionContext second = auctionRoomWithDeadline("ROOM01", Instant.parse("2026-04-09T00:00:20Z")); RoomAuctionDeadlineScheduler scheduler = new RoomAuctionDeadlineScheduler( taskScheduler, settleAuction, - new JpaAuctionScheduleReader(new RecordingScheduleRepository()), + new RecordingScheduleReader(), Clock.fixed(NOW, ZoneOffset.UTC), new InMemoryGames(first.game(), second.game()) ); @@ -85,13 +82,13 @@ class RoomAuctionDeadlineSchedulerTest { @Test void 재시작_시점에_지난_마감은_즉시_정산하고_미래_마감은_다시_예약한다() { FakeTaskScheduler taskScheduler = new FakeTaskScheduler(); - SettleAuction settleAuction = mock(SettleAuction.class); - JpaAuctionScheduleReader scheduleReader = - new JpaAuctionScheduleReader(new RecordingScheduleRepository( + RecordingSettleAuction settleAuction = new RecordingSettleAuction(); + AuctionScheduleReader scheduleReader = + new RecordingScheduleReader( schedule("DUE01", Instant.parse("2026-04-09T00:00:05Z")), schedule("FUTURE01", Instant.parse("2026-04-09T00:00:15Z")) - )); - given(settleAuction.settleIfDue("DUE01")).willReturn(completedAuctionRoom("DUE01")); + ); + settleAuction.returnRoom("DUE01", completedAuctionRoom("DUE01")); RoomAuctionDeadlineScheduler scheduler = new RoomAuctionDeadlineScheduler( taskScheduler, @@ -103,25 +100,25 @@ class RoomAuctionDeadlineSchedulerTest { scheduler.catchUpAndReschedule(); - verify(settleAuction).settleIfDue("DUE01"); + assertThat(settleAuction.settledCodes()).containsExactly("DUE01"); assertThat(taskScheduler.activeScheduledInstants()).containsExactly(Instant.parse("2026-04-09T00:00:15Z")); } @Test void 재시작_정산_중_손상된_방이_있어도_다음_방은_계속_처리한다() { FakeTaskScheduler taskScheduler = new FakeTaskScheduler(); - SettleAuction settleAuction = mock(SettleAuction.class); - JpaAuctionScheduleReader scheduleReader = - new JpaAuctionScheduleReader(new RecordingScheduleRepository( + RecordingSettleAuction settleAuction = new RecordingSettleAuction(); + AuctionScheduleReader scheduleReader = + new RecordingScheduleReader( List.of( schedule("BROKEN01", Instant.parse("2026-04-09T00:00:05Z")), schedule("DUE02", Instant.parse("2026-04-09T00:00:06Z")), schedule("FUTURE01", Instant.parse("2026-04-09T00:00:15Z")) ) - )); + ); StartedAuctionContext due02 = auctionRoomWithDeadline("DUE02", Instant.parse("2026-04-09T00:00:25Z")); - given(settleAuction.settleIfDue("BROKEN01")).willThrow(RoomStateInvalidException.auctionRoundMissing()); - given(settleAuction.settleIfDue("DUE02")).willReturn(due02.room()); + settleAuction.throwFailure("BROKEN01", RoomStateInvalidException.auctionRoundMissing()); + settleAuction.returnRoom("DUE02", due02.room()); RoomAuctionDeadlineScheduler scheduler = new RoomAuctionDeadlineScheduler( taskScheduler, @@ -133,8 +130,7 @@ class RoomAuctionDeadlineSchedulerTest { scheduler.catchUpAndReschedule(); - verify(settleAuction).settleIfDue("BROKEN01"); - verify(settleAuction).settleIfDue("DUE02"); + assertThat(settleAuction.settledCodes()).containsExactly("BROKEN01", "DUE02"); assertThat(taskScheduler.activeScheduledInstants()) .containsExactly( Instant.parse("2026-04-09T00:00:25Z"), @@ -142,65 +138,6 @@ class RoomAuctionDeadlineSchedulerTest { ); } - @Test - void 재시작_정산은_저장소_정렬보다_마감_시각을_우선한다() { - FakeTaskScheduler taskScheduler = new FakeTaskScheduler(); - SettleAuction settleAuction = mock(SettleAuction.class); - JpaAuctionScheduleReader scheduleReader = - new JpaAuctionScheduleReader(new RecordingScheduleRepository( - List.of( - schedule("FUTURE01", Instant.parse("2026-04-09T00:00:15Z")), - schedule("DUE01", Instant.parse("2026-04-09T00:00:05Z")), - schedule("LEGACY01", Instant.parse("2026-04-09T00:00:01Z")) - ) - )); - StartedAuctionContext legacy01 = auctionRoomWithDeadline("LEGACY01", Instant.parse("2026-04-09T00:00:20Z")); - given(settleAuction.settleIfDue("LEGACY01")).willReturn(legacy01.room()); - given(settleAuction.settleIfDue("DUE01")).willReturn(completedAuctionRoom("DUE01")); - RoomAuctionDeadlineScheduler scheduler = - new RoomAuctionDeadlineScheduler( - taskScheduler, - settleAuction, - scheduleReader, - Clock.fixed(NOW, ZoneOffset.UTC), - new InMemoryGames(legacy01.game()) - ); - - scheduler.catchUpAndReschedule(); - - InOrder inOrder = inOrder(settleAuction); - inOrder.verify(settleAuction).settleIfDue("LEGACY01"); - inOrder.verify(settleAuction).settleIfDue("DUE01"); - assertThat(taskScheduler.activeScheduledInstants()) - .containsExactly( - Instant.parse("2026-04-09T00:00:20Z"), - Instant.parse("2026-04-09T00:00:15Z") - ); - } - - @Test - void 재시작_예약은_첫_페이지를_넘는_미래_마감도_모두_처리한다() { - FakeTaskScheduler taskScheduler = new FakeTaskScheduler(); - SettleAuction settleAuction = mock(SettleAuction.class); - JpaAuctionScheduleReader scheduleReader = new JpaAuctionScheduleReader(new RecordingScheduleRepository(manyFutureSchedules(205))); - RoomAuctionDeadlineScheduler scheduler = - new RoomAuctionDeadlineScheduler( - taskScheduler, - settleAuction, - scheduleReader, - Clock.fixed(NOW, ZoneOffset.UTC), - new InMemoryGames() - ); - - scheduler.catchUpAndReschedule(); - - assertThat(taskScheduler.activeScheduledInstants()).hasSize(205); - assertThat(taskScheduler.activeScheduledInstants()).contains( - Instant.parse("2026-04-09T00:01:00Z"), - Instant.parse("2026-04-09T00:04:24Z") - ); - } - private static StartedAuctionContext auctionRoomWithDeadline(String code, Instant deadline) { Room room = Room.createFromTemplate( @@ -225,53 +162,78 @@ private static StartedAuctionContext auctionRoomWithDeadline(String code, Instan Instant.parse("2026-04-09T00:00:00Z") ); room.join(new TeamLeaderId("guest-" + code), "게스트", "guest-token-" + code); - GameId gameId = new GameId(java.util.UUID.randomUUID()); - StartedGameSnapshot snapshot = room.start( + GameId gameId = new GameId(UUID.randomUUID()); + room.start( new TeamLeaderId("host-" + code), gameId, Instant.parse("2026-04-09T00:00:00Z") ); - AuctionGame game = (AuctionGame) new GameFactory().create(snapshot); - setCurrentAuctionRoundEndsAt(game, deadline); + AuctionGame game = + new AuctionGame( + gameId, + room.getId(), + room.getCode(), + room.getGameType(), + room.getStartedAt(), + GameRules.auction( + room.getTeamCount(), + room.getTeamSize(), + room.getBudget(), + room.getPickBanTime(), + room.getMinBidUnit() + ), + List.of( + new AuctionParticipant(new TeamLeaderId("host-" + code), "호스트", 300), + new AuctionParticipant(new TeamLeaderId("guest-" + code), "게스트", 300) + ), + room.getPlayers().stream() + .map(player -> new GamePlayer(player.getId(), player.getName(), player.getPosition(), player.getDisplayOrder())) + .toList(), + 1, + deadline + ); return new StartedAuctionContext(room, game); } - private static void setCurrentAuctionRoundEndsAt(AuctionGame game, Instant deadline) { - try { - var field = AuctionGame.class.getDeclaredField("currentRoundEndsAt"); - field.setAccessible(true); - field.set(game, deadline); - } catch (ReflectiveOperationException ex) { - throw new AssertionError(ex); - } - } - private static Room completedAuctionRoom(String code) { return auctionRoomWithDeadline(code, Instant.parse("2026-04-09T00:00:05Z")).room(); } - private static ScheduledAuctionGame schedule(String code, Instant deadline) { - return new ScheduledAuctionGame(code, deadline); - } - - private static List manyFutureSchedules(int count) { - List candidates = new ArrayList<>(); - for (int index = 0; index < count; index++) { - int second = 60 + index; - candidates.add( - schedule( - "ROOM%03d".formatted(index), - Instant.parse("2026-04-09T00:%02d:%02dZ".formatted(second / 60, second % 60)) - ) - ); - } - return candidates; + private static AuctionScheduleCandidate schedule(String code, Instant deadline) { + return new AuctionScheduleCandidate(code, deadline); } private record StartedAuctionContext(Room room, AuctionGame game) { } - private record ScheduledAuctionGame(String code, Instant deadline) { + private static final class RecordingSettleAuction implements AuctionSettlementRunner { + private final Map outcomes = new LinkedHashMap<>(); + private final List settledCodes = new ArrayList<>(); + + private void returnRoom(String code, Room room) { + outcomes.put(code, room); + } + + private void throwFailure(String code, RuntimeException failure) { + outcomes.put(code, failure); + } + + @Override + public Room settleIfDue(String code) { + settledCodes.add(code); + Object outcome = outcomes.get(code); + if (outcome instanceof RuntimeException failure) { + throw failure; + } + if (outcome instanceof Room room) { + return room; + } + throw new AssertionError("No settlement outcome registered for " + code); + } + + private List settledCodes() { + return settledCodes; + } } private static final class InMemoryGames implements Games { @@ -295,27 +257,20 @@ public java.util.Optional findById(GameId id) { } } - private static final class RecordingScheduleRepository implements AuctionScheduleJpaRepository { - private final List games; + private static final class RecordingScheduleReader implements AuctionScheduleReader { + private final List candidates; - private RecordingScheduleRepository(ScheduledAuctionGame... games) { - this.games = List.of(games); + private RecordingScheduleReader(AuctionScheduleCandidate... candidates) { + this.candidates = List.of(candidates); } - private RecordingScheduleRepository(List games) { - this.games = List.copyOf(games); + private RecordingScheduleReader(List candidates) { + this.candidates = List.copyOf(candidates); } @Override - public List findByCurrentRoundEndsAtNotNullOrderByRoomCodeAsc(Pageable pageable) { - int fromIndex = (int) pageable.getOffset(); - if (fromIndex >= games.size()) { - return List.of(); - } - int toIndex = Math.min(fromIndex + pageable.getPageSize(), games.size()); - return games.subList(fromIndex, toIndex).stream() - .map(candidate -> auctionRoomWithDeadline(candidate.code(), candidate.deadline()).game()) - .toList(); + public List findInProgressAuctionSchedules() { + return candidates; } } } diff --git a/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/schedule/RoomAuctionSchedulingPolicyTest.java b/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/schedule/RoomAuctionSchedulingPolicyTest.java deleted file mode 100644 index 0ed71c18..00000000 --- a/src/test/java/com/naminhyeok/fantazzk/room/infrastructure/schedule/RoomAuctionSchedulingPolicyTest.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.naminhyeok.fantazzk.room.infrastructure.schedule; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.naminhyeok.fantazzk.room.domain.AuctionOutcome; -import com.naminhyeok.fantazzk.room.domain.AuctionSettled; -import com.naminhyeok.fantazzk.room.domain.AuctionGame; -import com.naminhyeok.fantazzk.room.domain.BidPlaced; -import com.naminhyeok.fantazzk.room.domain.Game; -import com.naminhyeok.fantazzk.room.domain.GameId; -import com.naminhyeok.fantazzk.room.domain.RoomStarted; -import com.naminhyeok.fantazzk.room.infrastructure.persistence.AuctionScheduleJpaRepository; -import com.naminhyeok.fantazzk.room.infrastructure.persistence.JpaAuctionScheduleReader; -import com.naminhyeok.fantazzk.room.infrastructure.schedule.RoomAuctionDeadlineScheduler; -import com.naminhyeok.fantazzk.room.infrastructure.schedule.RoomAuctionSchedulingPolicy; -import com.naminhyeok.fantazzk.room.repository.Games; -import java.time.Clock; -import java.time.Instant; -import java.time.ZoneOffset; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.springframework.data.domain.Pageable; - -class RoomAuctionSchedulingPolicyTest { - @Test - void 경매_시작_이벤트는_첫_마감_정산을_예약한다() { - FakeTaskScheduler taskScheduler = new FakeTaskScheduler(); - RoomAuctionSchedulingPolicy policy = new RoomAuctionSchedulingPolicy( - new RoomAuctionDeadlineScheduler( - taskScheduler, - null, - new JpaAuctionScheduleReader(new EmptyScheduleRepository()), - Clock.fixed(Instant.EPOCH, ZoneOffset.UTC), - emptyGames() - ) - ); - - policy.on(new RoomStarted("ROOM01", Instant.parse("2026-04-09T00:00:15Z"))); - - assertThat(taskScheduler.scheduledInstants()).containsExactly(Instant.parse("2026-04-09T00:00:15Z")); - } - - @Test - void 입찰_이벤트는_기존_마감을_새_마감으로_교체한다() { - FakeTaskScheduler taskScheduler = new FakeTaskScheduler(); - RoomAuctionDeadlineScheduler scheduler = - new RoomAuctionDeadlineScheduler( - taskScheduler, - null, - new JpaAuctionScheduleReader(new EmptyScheduleRepository()), - Clock.fixed(Instant.EPOCH, ZoneOffset.UTC), - emptyGames() - ); - scheduler.refresh("ROOM01", Instant.parse("2026-04-09T00:00:15Z")); - RoomAuctionSchedulingPolicy policy = new RoomAuctionSchedulingPolicy(scheduler); - - policy.on(new BidPlaced("ROOM01", "leader-1", 150, 1, Instant.parse("2026-04-09T00:00:30Z"))); - - assertThat(taskScheduler.cancelledInstants()).containsExactly(Instant.parse("2026-04-09T00:00:15Z")); - assertThat(taskScheduler.activeScheduledInstants()).containsExactly(Instant.parse("2026-04-09T00:00:30Z")); - } - - @Test - void 정산_이벤트에_다음_마감이_없으면_예약을_해제한다() { - FakeTaskScheduler taskScheduler = new FakeTaskScheduler(); - RoomAuctionDeadlineScheduler scheduler = - new RoomAuctionDeadlineScheduler( - taskScheduler, - null, - new JpaAuctionScheduleReader(new EmptyScheduleRepository()), - Clock.fixed(Instant.EPOCH, ZoneOffset.UTC), - emptyGames() - ); - scheduler.refresh("ROOM01", Instant.parse("2026-04-09T00:00:30Z")); - RoomAuctionSchedulingPolicy policy = new RoomAuctionSchedulingPolicy(scheduler); - - policy.on(new AuctionSettled("ROOM01", AuctionOutcome.SOLD.name(), null)); - - assertThat(taskScheduler.cancelledInstants()).containsExactly(Instant.parse("2026-04-09T00:00:30Z")); - assertThat(taskScheduler.activeScheduledInstants()).isEmpty(); - } - - private static Games emptyGames() { - return new Games() { - @Override - public Game save(Game game) { - throw new UnsupportedOperationException(); - } - - @Override - public java.util.Optional findById(GameId id) { - return java.util.Optional.empty(); - } - }; - } - - private static final class EmptyScheduleRepository implements AuctionScheduleJpaRepository { - @Override - public List findByCurrentRoundEndsAtNotNullOrderByRoomCodeAsc(Pageable pageable) { - return List.of(); - } - } - -} diff --git a/src/test/java/com/naminhyeok/fantazzk/room/support/RoomApiTestFixtures.java b/src/test/java/com/naminhyeok/fantazzk/room/support/RoomApiTestFixtures.java index ff8758a9..61d4b7a6 100644 --- a/src/test/java/com/naminhyeok/fantazzk/room/support/RoomApiTestFixtures.java +++ b/src/test/java/com/naminhyeok/fantazzk/room/support/RoomApiTestFixtures.java @@ -1,7 +1,6 @@ package com.naminhyeok.fantazzk.room.support; import com.naminhyeok.fantazzk.room.domain.AuctionGame; -import com.naminhyeok.fantazzk.room.domain.DraftGame; import com.naminhyeok.fantazzk.room.domain.DraftOrderStrategy; import com.naminhyeok.fantazzk.room.domain.GameFactory; import com.naminhyeok.fantazzk.room.domain.GameId; @@ -108,10 +107,6 @@ public static StartedRoomSnapshot startedAuctionDetails() { return new StartedRoomSnapshot(room, startedAuctionGame(room)); } - public static Room inProgressAuctionRoom() { - return inProgressAuctionDetails().room(); - } - public static StartedRoomSnapshot inProgressAuctionDetails() { Room room = Room.createFromTemplate( @@ -145,45 +140,6 @@ public static StartedRoomSnapshot inProgressAuctionDetails() { return new StartedRoomSnapshot(room, game); } - public static Room inProgressDraftRoom() { - return inProgressDraftDetails().room(); - } - - public static StartedRoomSnapshot inProgressDraftDetails() { - Room room = - Room.createFromTemplate( - ROOM_CODE, - new TeamLeaderId(HOST_ID), - "호스트", - HOST_TOKEN, - new RoomTemplateSpec( - "LEAGUE_OF_LEGENDS", - RoomMode.DRAFT, - 2, - 3, - null, - 30, - null, - DraftOrderStrategy.SNAKE, - List.of( - new RoomTemplateSpec.Player(new RoomPlayerId(0), "선수1", "TOP", 0), - new RoomTemplateSpec.Player(new RoomPlayerId(1), "선수2", "JUNGLE", 1), - new RoomTemplateSpec.Player(new RoomPlayerId(2), "선수3", "MID", 2), - new RoomTemplateSpec.Player(new RoomPlayerId(3), "선수4", "ADC", 3) - ) - ), - CREATED_AT - ); - room.join(new TeamLeaderId(GUEST_ID), "게스트", GUEST_TOKEN); - room.selectDraftPosition(new TeamLeaderId(HOST_ID), 1); - room.selectDraftPosition(new TeamLeaderId(GUEST_ID), 2); - StartedGameSnapshot snapshot = room.start(new TeamLeaderId(HOST_ID), new GameId(UUID.fromString(DRAFT_GAME_ID)), CREATED_AT); - DraftGame game = (DraftGame) new GameFactory().create(snapshot); - game.pick(new TeamLeaderId(HOST_ID), "선수1"); - game.pick(new TeamLeaderId(GUEST_ID), "선수2"); - return new StartedRoomSnapshot(room, game); - } - private static AuctionGame startedAuctionGame(Room room) { StartedGameSnapshot snapshot = new StartedGameSnapshot( room.getId(), diff --git a/src/test/java/com/naminhyeok/fantazzk/room/web/GameAuctionApiControllerWebMvcTest.java b/src/test/java/com/naminhyeok/fantazzk/room/web/GameAuctionApiControllerWebMvcTest.java index 42a392d6..425c3a88 100644 --- a/src/test/java/com/naminhyeok/fantazzk/room/web/GameAuctionApiControllerWebMvcTest.java +++ b/src/test/java/com/naminhyeok/fantazzk/room/web/GameAuctionApiControllerWebMvcTest.java @@ -1,21 +1,16 @@ package com.naminhyeok.fantazzk.room.web; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import com.naminhyeok.fantazzk.CoreException; import com.naminhyeok.fantazzk.ErrorMessage; import com.naminhyeok.fantazzk.GlobalExceptionHandler; import com.naminhyeok.fantazzk.room.application.PlaceBid; -import com.naminhyeok.fantazzk.room.domain.AuctionBid; -import com.naminhyeok.fantazzk.room.domain.BidSequence; import com.naminhyeok.fantazzk.room.domain.RoomErrorType; import com.naminhyeok.fantazzk.room.domain.RoomStateInvalidException; -import com.naminhyeok.fantazzk.room.domain.TeamLeaderId; -import com.naminhyeok.fantazzk.room.web.GameAuctionApiController; +import com.naminhyeok.fantazzk.room.support.RoomApiTestFixtures; import java.util.UUID; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -27,9 +22,7 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.assertj.MockMvcTester; -import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; -import com.naminhyeok.fantazzk.room.support.RoomApiTestFixtures; @WebMvcTest(GameAuctionApiController.class) @AutoConfigureMockMvc(addFilters = false) @@ -45,11 +38,7 @@ class GameAuctionApiControllerWebMvcTest { private PlaceBid placeBid; @Test - void 입찰_API는_액션_토큰이_있으면_빈_SUCCESS를_반환한다() throws Exception { - UUID gameId = UUID.fromString(RoomApiTestFixtures.GAME_ID); - given(placeBid.place(gameId, RoomApiTestFixtures.HOST_TOKEN, 150)) - .willReturn(new AuctionBid(1, new BidSequence(1), new TeamLeaderId(RoomApiTestFixtures.HOST_ID), 150)); - + void 입찰_API는_액션_토큰이_있으면_성공한다() throws Exception { var result = mockMvcTester().perform( post("/api/v1/games/{gameId}/bids", RoomApiTestFixtures.GAME_ID) .header("X-Room-Action-Token", RoomApiTestFixtures.HOST_TOKEN) @@ -64,11 +53,6 @@ class GameAuctionApiControllerWebMvcTest { ); result.assertThat().hasStatusOk(); - JsonNode body = objectMapper.readTree(result.getResponse().getContentAsString()); - assertThat(body.at("/resultType").asText()).isEqualTo("SUCCESS"); - assertThat(body.at("/success").isNull()).isTrue(); - assertThat(body.at("/error").isNull()).isTrue(); - verify(placeBid).place(gameId, RoomApiTestFixtures.HOST_TOKEN, 150); } @Test diff --git a/src/test/java/com/naminhyeok/fantazzk/room/web/GameDraftApiControllerWebMvcTest.java b/src/test/java/com/naminhyeok/fantazzk/room/web/GameDraftApiControllerWebMvcTest.java index c0fc66d3..a4b75ab2 100644 --- a/src/test/java/com/naminhyeok/fantazzk/room/web/GameDraftApiControllerWebMvcTest.java +++ b/src/test/java/com/naminhyeok/fantazzk/room/web/GameDraftApiControllerWebMvcTest.java @@ -1,20 +1,15 @@ package com.naminhyeok.fantazzk.room.web; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import com.naminhyeok.fantazzk.CoreException; import com.naminhyeok.fantazzk.ErrorMessage; import com.naminhyeok.fantazzk.GlobalExceptionHandler; import com.naminhyeok.fantazzk.room.application.PickDraft; -import com.naminhyeok.fantazzk.room.domain.Room; import com.naminhyeok.fantazzk.room.domain.RoomErrorType; -import com.naminhyeok.fantazzk.room.domain.RosterMember; -import com.naminhyeok.fantazzk.room.domain.TeamLeaderId; -import com.naminhyeok.fantazzk.room.web.GameDraftApiController; +import com.naminhyeok.fantazzk.room.support.RoomApiTestFixtures; import java.util.UUID; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -26,9 +21,7 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.assertj.MockMvcTester; -import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; -import com.naminhyeok.fantazzk.room.support.RoomApiTestFixtures; @WebMvcTest(GameDraftApiController.class) @AutoConfigureMockMvc(addFilters = false) @@ -44,11 +37,7 @@ class GameDraftApiControllerWebMvcTest { private PickDraft pickDraft; @Test - void 드래프트_픽_API는_액션_토큰이_있으면_빈_SUCCESS를_반환한다() throws Exception { - UUID gameId = UUID.fromString(RoomApiTestFixtures.DRAFT_GAME_ID); - given(pickDraft.pick(gameId, RoomApiTestFixtures.GUEST_TOKEN, "선수3")) - .willReturn(new RosterMember(new TeamLeaderId(RoomApiTestFixtures.GUEST_ID), "선수3", 2)); - + void 드래프트_픽_API는_액션_토큰이_있으면_성공한다() throws Exception { var result = mockMvcTester().perform( post("/api/v1/games/{gameId}/draft-picks", RoomApiTestFixtures.DRAFT_GAME_ID) .header("X-Room-Action-Token", RoomApiTestFixtures.GUEST_TOKEN) @@ -63,11 +52,6 @@ class GameDraftApiControllerWebMvcTest { ); result.assertThat().hasStatusOk(); - JsonNode body = objectMapper.readTree(result.getResponse().getContentAsString()); - assertThat(body.at("/resultType").asText()).isEqualTo("SUCCESS"); - assertThat(body.at("/success").isNull()).isTrue(); - assertThat(body.at("/error").isNull()).isTrue(); - verify(pickDraft).pick(gameId, RoomApiTestFixtures.GUEST_TOKEN, "선수3"); } @Test diff --git a/src/test/java/com/naminhyeok/fantazzk/room/web/GameQueryApiControllerWebMvcTest.java b/src/test/java/com/naminhyeok/fantazzk/room/web/GameQueryApiControllerWebMvcTest.java index 9ca8e0c9..0643322d 100644 --- a/src/test/java/com/naminhyeok/fantazzk/room/web/GameQueryApiControllerWebMvcTest.java +++ b/src/test/java/com/naminhyeok/fantazzk/room/web/GameQueryApiControllerWebMvcTest.java @@ -9,6 +9,7 @@ import com.naminhyeok.fantazzk.GlobalExceptionHandler; import com.naminhyeok.fantazzk.room.domain.RoomErrorType; import com.naminhyeok.fantazzk.room.query.GetGame; +import com.naminhyeok.fantazzk.room.support.RoomApiTestFixtures; import com.naminhyeok.fantazzk.room.web.GameQueryApiController; import java.util.UUID; import org.junit.jupiter.api.Test; @@ -22,7 +23,6 @@ import org.springframework.test.web.servlet.assertj.MockMvcTester; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; -import com.naminhyeok.fantazzk.room.support.RoomApiTestFixtures; @WebMvcTest(GameQueryApiController.class) @AutoConfigureMockMvc(addFilters = false) @@ -52,10 +52,6 @@ class GameQueryApiControllerWebMvcTest { assertThat(body.at("/success/playerPool/0/name").asText()).isEqualTo("선수1"); assertThat(body.at("/success/roster/0/playerName").asText()).isEqualTo("선수1"); assertThat(body.at("/success/auctionProgress/currentRound").asInt()).isEqualTo(2); - assertThat(body.at("/success/id").isMissingNode()).isTrue(); - assertThat(body.at("/success/players").isMissingNode()).isTrue(); - assertThat(body.at("/success/members").isMissingNode()).isTrue(); - assertThat(body.at("/success/progress").isMissingNode()).isTrue(); } @Test diff --git a/src/test/java/com/naminhyeok/fantazzk/room/web/RoomDraftApiControllerWebMvcTest.java b/src/test/java/com/naminhyeok/fantazzk/room/web/RoomDraftApiControllerWebMvcTest.java index ff0a9a14..12706785 100644 --- a/src/test/java/com/naminhyeok/fantazzk/room/web/RoomDraftApiControllerWebMvcTest.java +++ b/src/test/java/com/naminhyeok/fantazzk/room/web/RoomDraftApiControllerWebMvcTest.java @@ -1,7 +1,6 @@ package com.naminhyeok.fantazzk.room.web; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doThrow; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; @@ -11,9 +10,8 @@ import com.naminhyeok.fantazzk.GlobalExceptionHandler; import com.naminhyeok.fantazzk.room.application.ClearDraftPosition; import com.naminhyeok.fantazzk.room.application.SelectDraftPosition; -import com.naminhyeok.fantazzk.room.domain.Room; import com.naminhyeok.fantazzk.room.domain.RoomErrorType; -import com.naminhyeok.fantazzk.room.web.RoomDraftApiController; +import com.naminhyeok.fantazzk.room.support.RoomApiTestFixtures; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; @@ -24,9 +22,7 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.assertj.MockMvcTester; -import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; -import com.naminhyeok.fantazzk.room.support.RoomApiTestFixtures; @WebMvcTest(RoomDraftApiController.class) @AutoConfigureMockMvc(addFilters = false) @@ -45,10 +41,7 @@ class RoomDraftApiControllerWebMvcTest { private ClearDraftPosition clearDraftPosition; @Test - void 드래프트_자리_선택_API는_액션_토큰이_있으면_빈_SUCCESS를_반환한다() throws Exception { - given(selectDraftPosition.select(RoomApiTestFixtures.ROOM_CODE, RoomApiTestFixtures.GUEST_TOKEN, 2)) - .willReturn(null); - + void 드래프트_자리_선택_API는_액션_토큰이_있으면_성공한다() throws Exception { var result = mockMvcTester().perform( put("/api/v1/rooms/{code}/draft-position", RoomApiTestFixtures.ROOM_CODE) .header("X-Room-Action-Token", RoomApiTestFixtures.GUEST_TOKEN) @@ -63,27 +56,16 @@ class RoomDraftApiControllerWebMvcTest { ); result.assertThat().hasStatusOk(); - JsonNode body = objectMapper.readTree(result.getResponse().getContentAsString()); - assertThat(body.at("/resultType").asText()).isEqualTo("SUCCESS"); - assertThat(body.at("/success").isNull()).isTrue(); - assertThat(body.at("/error").isNull()).isTrue(); } @Test - void 드래프트_자리_취소_API는_액션_토큰이_있으면_빈_SUCCESS를_반환한다() throws Exception { - given(clearDraftPosition.clear(RoomApiTestFixtures.ROOM_CODE, RoomApiTestFixtures.HOST_TOKEN)) - .willReturn(null); - + void 드래프트_자리_취소_API는_액션_토큰이_있으면_성공한다() throws Exception { var result = mockMvcTester().perform( delete("/api/v1/rooms/{code}/draft-position", RoomApiTestFixtures.ROOM_CODE) .header("X-Room-Action-Token", RoomApiTestFixtures.HOST_TOKEN) ); result.assertThat().hasStatusOk(); - JsonNode body = objectMapper.readTree(result.getResponse().getContentAsString()); - assertThat(body.at("/resultType").asText()).isEqualTo("SUCCESS"); - assertThat(body.at("/success").isNull()).isTrue(); - assertThat(body.at("/error").isNull()).isTrue(); } @Test diff --git a/src/test/java/com/naminhyeok/fantazzk/room/web/RoomQueryApiControllerWebMvcTest.java b/src/test/java/com/naminhyeok/fantazzk/room/web/RoomQueryApiControllerWebMvcTest.java index 96da5705..7d2fba3d 100644 --- a/src/test/java/com/naminhyeok/fantazzk/room/web/RoomQueryApiControllerWebMvcTest.java +++ b/src/test/java/com/naminhyeok/fantazzk/room/web/RoomQueryApiControllerWebMvcTest.java @@ -12,6 +12,7 @@ import com.naminhyeok.fantazzk.room.query.FindJoinableRooms; import com.naminhyeok.fantazzk.room.query.GetRoom; import com.naminhyeok.fantazzk.room.query.JoinableRoomResponse; +import com.naminhyeok.fantazzk.room.support.RoomApiTestFixtures; import com.naminhyeok.fantazzk.room.web.RoomQueryApiController; import java.time.Instant; import java.util.List; @@ -26,7 +27,6 @@ import org.springframework.test.web.servlet.assertj.MockMvcTester; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; -import com.naminhyeok.fantazzk.room.support.RoomApiTestFixtures; @WebMvcTest(RoomQueryApiController.class) @AutoConfigureMockMvc(addFilters = false) @@ -60,13 +60,6 @@ class RoomQueryApiControllerWebMvcTest { assertThat(body.at("/success/leaders/0/nickname").asText()).isEqualTo("호스트"); assertThat(body.at("/success/playerPool/0/name").asText()).isEqualTo("선수1"); assertThat(body.at("/success/draftOrder/slots/0/leaderId").asText()).isEqualTo(RoomApiTestFixtures.HOST_ID); - assertThat(body.at("/success/code").isMissingNode()).isTrue(); - assertThat(body.at("/success/teamLeaders").isMissingNode()).isTrue(); - assertThat(body.at("/success/players").isMissingNode()).isTrue(); - assertThat(body.at("/success/members").isMissingNode()).isTrue(); - assertThat(body.at("/success/auctionProgress").isMissingNode()).isTrue(); - assertThat(body.at("/success/draftProgress").isMissingNode()).isTrue(); - assertThat(result.getResponse().getContentAsString()).doesNotContain("teamLeaderSession"); } @Test @@ -79,9 +72,6 @@ class RoomQueryApiControllerWebMvcTest { JsonNode body = objectMapper.readTree(result.getResponse().getContentAsString()); assertThat(body.at("/success/status").asText()).isEqualTo("STARTED"); assertThat(body.at("/success/startedGameId").asText()).isEqualTo(RoomApiTestFixtures.GAME_ID); - assertThat(body.at("/success/auctionProgress").isMissingNode()).isTrue(); - assertThat(body.at("/success/draftProgress").isMissingNode()).isTrue(); - assertThat(body.at("/success/members").isMissingNode()).isTrue(); } @Test diff --git a/src/test/java/com/naminhyeok/fantazzk/room/web/RoomSessionApiControllerWebMvcTest.java b/src/test/java/com/naminhyeok/fantazzk/room/web/RoomSessionApiControllerWebMvcTest.java index f44eb27e..c07fc48c 100644 --- a/src/test/java/com/naminhyeok/fantazzk/room/web/RoomSessionApiControllerWebMvcTest.java +++ b/src/test/java/com/naminhyeok/fantazzk/room/web/RoomSessionApiControllerWebMvcTest.java @@ -15,9 +15,7 @@ import com.naminhyeok.fantazzk.room.domain.Room; import com.naminhyeok.fantazzk.room.domain.RoomErrorType; import com.naminhyeok.fantazzk.room.domain.RoomTeamLeader; -import com.naminhyeok.fantazzk.room.domain.TeamLeaderRole; -import com.naminhyeok.fantazzk.room.web.RoomJoinResponse; -import com.naminhyeok.fantazzk.room.web.RoomSessionApiController; +import com.naminhyeok.fantazzk.room.support.RoomApiTestFixtures; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; @@ -28,8 +26,8 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.assertj.MockMvcTester; +import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; -import com.naminhyeok.fantazzk.room.support.RoomApiTestFixtures; @WebMvcTest(RoomSessionApiController.class) @AutoConfigureMockMvc(addFilters = false) @@ -68,15 +66,12 @@ class RoomSessionApiControllerWebMvcTest { ); result.assertThat().hasStatus(HttpStatus.CREATED); - RoomSessionApiResponse body = readBody(result, RoomSessionApiResponse.class); - assertThat(body.resultType()).isEqualTo("SUCCESS"); - assertThat(body.success().room().roomCode()).isEqualTo(RoomApiTestFixtures.ROOM_CODE); - assertThat(body.success().room().status()).isEqualTo("WAITING"); - assertThat(body.success().room().leaders()).hasSize(1); - assertThat(body.success().room().playerPool()).hasSize(2); - assertThat(body.success().teamLeaderSession().leaderId()).isEqualTo(RoomApiTestFixtures.HOST_ID); - assertThat(body.success().teamLeaderSession().role()).isEqualTo(TeamLeaderRole.HOST); - assertThat(body.success().teamLeaderSession().actionToken()).isEqualTo(RoomApiTestFixtures.HOST_TOKEN); + JsonNode body = readJsonBody(result); + assertThat(body.at("/resultType").asText()).isEqualTo("SUCCESS"); + assertThat(body.at("/success/room/roomCode").asText()).isEqualTo(RoomApiTestFixtures.ROOM_CODE); + assertThat(body.at("/success/teamLeaderSession/leaderId").asText()).isEqualTo(RoomApiTestFixtures.HOST_ID); + assertThat(body.at("/success/teamLeaderSession/role").asText()).isEqualTo("HOST"); + assertThat(body.at("/success/teamLeaderSession/actionToken").asText()).isEqualTo(RoomApiTestFixtures.HOST_TOKEN); } @Test @@ -121,14 +116,12 @@ class RoomSessionApiControllerWebMvcTest { ); result.assertThat().hasStatusOk(); - RoomSessionApiResponse body = readBody(result, RoomSessionApiResponse.class); - assertThat(body.resultType()).isEqualTo("SUCCESS"); - assertThat(body.success().room().roomCode()).isEqualTo(RoomApiTestFixtures.ROOM_CODE); - assertThat(body.success().room().leaders()).hasSize(2); - assertThat(body.success().room().playerPool()).hasSize(2); - assertThat(body.success().teamLeaderSession().leaderId()).isEqualTo(RoomApiTestFixtures.GUEST_ID); - assertThat(body.success().teamLeaderSession().role()).isEqualTo(TeamLeaderRole.LEADER); - assertThat(body.success().teamLeaderSession().actionToken()).isEqualTo(RoomApiTestFixtures.GUEST_TOKEN); + JsonNode body = readJsonBody(result); + assertThat(body.at("/resultType").asText()).isEqualTo("SUCCESS"); + assertThat(body.at("/success/room/roomCode").asText()).isEqualTo(RoomApiTestFixtures.ROOM_CODE); + assertThat(body.at("/success/teamLeaderSession/leaderId").asText()).isEqualTo(RoomApiTestFixtures.GUEST_ID); + assertThat(body.at("/success/teamLeaderSession/role").asText()).isEqualTo("LEADER"); + assertThat(body.at("/success/teamLeaderSession/actionToken").asText()).isEqualTo(RoomApiTestFixtures.GUEST_TOKEN); } @Test @@ -160,7 +153,9 @@ private T readBody(org.springframework.test.web.servlet.assertj.MvcTestResul return objectMapper.readValue(result.getResponse().getContentAsString(), bodyType); } - private record RoomSessionApiResponse(String resultType, RoomJoinResponse success, ErrorMessage error) {} + private JsonNode readJsonBody(org.springframework.test.web.servlet.assertj.MvcTestResult result) throws Exception { + return objectMapper.readTree(result.getResponse().getContentAsString()); + } private record VoidApiResponse(String resultType, Void success, ErrorMessage error) {} } diff --git a/src/test/java/com/naminhyeok/fantazzk/room/web/RoomStartApiControllerWebMvcTest.java b/src/test/java/com/naminhyeok/fantazzk/room/web/RoomStartApiControllerWebMvcTest.java index 272e3577..2b86f501 100644 --- a/src/test/java/com/naminhyeok/fantazzk/room/web/RoomStartApiControllerWebMvcTest.java +++ b/src/test/java/com/naminhyeok/fantazzk/room/web/RoomStartApiControllerWebMvcTest.java @@ -9,8 +9,8 @@ import com.naminhyeok.fantazzk.ErrorMessage; import com.naminhyeok.fantazzk.GlobalExceptionHandler; import com.naminhyeok.fantazzk.room.application.StartRoom; -import com.naminhyeok.fantazzk.room.domain.Room; import com.naminhyeok.fantazzk.room.domain.RoomErrorType; +import com.naminhyeok.fantazzk.room.support.RoomApiTestFixtures; import com.naminhyeok.fantazzk.room.web.RoomStartApiController; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -23,7 +23,6 @@ import org.springframework.test.web.servlet.assertj.MockMvcTester; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; -import com.naminhyeok.fantazzk.room.support.RoomApiTestFixtures; @WebMvcTest(RoomStartApiController.class) @AutoConfigureMockMvc(addFilters = false) @@ -52,9 +51,6 @@ class RoomStartApiControllerWebMvcTest { JsonNode body = objectMapper.readTree(result.getResponse().getContentAsString()); assertThat(body.at("/resultType").asText()).isEqualTo("SUCCESS"); assertThat(body.at("/success/gameId").asText()).isEqualTo(RoomApiTestFixtures.GAME_ID); - assertThat(body.at("/success/roomCode").isMissingNode()).isTrue(); - assertThat(body.at("/success/mode").isMissingNode()).isTrue(); - assertThat(body.at("/success/status").isMissingNode()).isTrue(); } @Test