From 5d31a08e83669432ac1d0fb7bbb4e3674c5b8b5e Mon Sep 17 00:00:00 2001 From: Ted Date: Sat, 29 Jul 2023 20:05:03 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BE=9D=E6=93=9A=E5=AF=A6=E4=BD=9C=E6=88=BF?= =?UTF-8?q?=E4=B8=BBIsReady=20always=20True=E4=BF=AE=E6=AD=A3=20=E4=BE=9D?= =?UTF-8?q?=E6=93=9Ajwt=20subject=20is=20user=20identity=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=20=E6=A0=B9=E6=93=9A=E7=AC=AC=E4=B8=89=E6=AC=A1=20code=20revie?= =?UTF-8?q?w=20=E4=BF=AE=E6=AD=A3=20=E6=A0=B9=E6=93=9A=E7=AC=AC=E4=BA=8C?= =?UTF-8?q?=E6=AC=A1=20code=20review=20=E4=BF=AE=E6=AD=A3=20=E6=A0=B9?= =?UTF-8?q?=E6=93=9A=E7=AC=AC=E4=B8=80=E6=AC=A1=20code=20review=20?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20=E5=AF=A6=E4=BD=9C=E5=8F=96=E5=BE=97?= =?UTF-8?q?=E6=88=BF=E9=96=93=E8=B3=87=E8=A8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/usecases/GetRoomUsecase.kt | 41 +++++++++++ .../kotlin/tw/waterballsa/gaas/domain/Room.kt | 3 + .../gaas/exceptions/enums/PlatformError.kt | 1 + .../gaas/spring/controllers/RoomController.kt | 17 ++++- .../controllers/presenter/GetRoomPresenter.kt | 36 ++++++++++ .../controllers/viewmodel/GetRoomViewModel.kt | 18 +++++ .../it/controllers/RoomControllerTest.kt | 69 +++++++++++++++++-- 7 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 application/src/main/kotlin/tw/waterballsa/gaas/application/usecases/GetRoomUsecase.kt create mode 100644 spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/presenter/GetRoomPresenter.kt create mode 100644 spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/viewmodel/GetRoomViewModel.kt diff --git a/application/src/main/kotlin/tw/waterballsa/gaas/application/usecases/GetRoomUsecase.kt b/application/src/main/kotlin/tw/waterballsa/gaas/application/usecases/GetRoomUsecase.kt new file mode 100644 index 00000000..0c64df28 --- /dev/null +++ b/application/src/main/kotlin/tw/waterballsa/gaas/application/usecases/GetRoomUsecase.kt @@ -0,0 +1,41 @@ +package tw.waterballsa.gaas.application.usecases + +import tw.waterballsa.gaas.application.eventbus.EventBus +import tw.waterballsa.gaas.application.repositories.RoomRepository +import tw.waterballsa.gaas.application.repositories.UserRepository +import tw.waterballsa.gaas.domain.Room +import tw.waterballsa.gaas.domain.Room.Player +import tw.waterballsa.gaas.exceptions.PlatformException +import tw.waterballsa.gaas.exceptions.enums.PlatformError.PLAYER_NOT_IN_ROOM_ERROR +import javax.inject.Named + +@Named +class GetRoomUsecase( + roomRepository: RoomRepository, + userRepository: UserRepository, + private val eventBus: EventBus, +) : AbstractRoomUseCase(roomRepository, userRepository) { + fun execute(request: Request, presenter: Presenter) { + with(request) { + val room = findRoomById(roomId) + val player = findPlayerByIdentity(userIdentity) + room.validatePlayerInRoom(player.id) + presenter.present(room) + } + } + + private fun Room.validatePlayerInRoom(playerId: Player.Id) { + if (!hasPlayer(playerId)) { + throw PlatformException(PLAYER_NOT_IN_ROOM_ERROR, "Player(${playerId.value}) is not in the room(${roomId!!.value}).") + } + } + + data class Request( + val roomId: String, + val userIdentity: String + ) + + interface Presenter { + fun present(room: Room) + } +} \ No newline at end of file diff --git a/domain/src/main/kotlin/tw/waterballsa/gaas/domain/Room.kt b/domain/src/main/kotlin/tw/waterballsa/gaas/domain/Room.kt index 271e743c..9c58ac94 100644 --- a/domain/src/main/kotlin/tw/waterballsa/gaas/domain/Room.kt +++ b/domain/src/main/kotlin/tw/waterballsa/gaas/domain/Room.kt @@ -39,6 +39,9 @@ class Room( } } + fun hasPlayer(playerId: Player.Id): Boolean = + players.any { it.id == playerId } + fun kickPlayer(hostId: Player.Id, playerId: Player.Id) { validateRoomHost(hostId) val player = diff --git a/domain/src/main/kotlin/tw/waterballsa/gaas/exceptions/enums/PlatformError.kt b/domain/src/main/kotlin/tw/waterballsa/gaas/exceptions/enums/PlatformError.kt index f5a2a214..dcf73a8a 100644 --- a/domain/src/main/kotlin/tw/waterballsa/gaas/exceptions/enums/PlatformError.kt +++ b/domain/src/main/kotlin/tw/waterballsa/gaas/exceptions/enums/PlatformError.kt @@ -26,4 +26,5 @@ enum class PlatformError( PLAYER_NOT_HOST("P002"), PLAYER_JOIN_ROOM_ERROR("P003"), PLAYER_CREATE_ROOM_ERROR("P004"), + PLAYER_NOT_IN_ROOM_ERROR("P005"), } diff --git a/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/RoomController.kt b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/RoomController.kt index 3c0f6ce0..c296d603 100644 --- a/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/RoomController.kt +++ b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/RoomController.kt @@ -11,7 +11,9 @@ import tw.waterballsa.gaas.domain.Room import tw.waterballsa.gaas.events.CreatedRoomEvent import tw.waterballsa.gaas.events.DomainEvent import tw.waterballsa.gaas.spring.controllers.RoomController.CreateRoomViewModel +import tw.waterballsa.gaas.spring.controllers.presenter.GetRoomPresenter import tw.waterballsa.gaas.spring.controllers.presenter.GetRoomsPresenter +import tw.waterballsa.gaas.spring.controllers.viewmodel.GetRoomViewModel import tw.waterballsa.gaas.spring.controllers.viewmodel.GetRoomsViewModel import tw.waterballsa.gaas.spring.controllers.viewmodel.PlatformViewModel import tw.waterballsa.gaas.spring.extensions.getEvent @@ -28,7 +30,8 @@ class RoomController( private val closeRoomsUseCase: CloseRoomUsecase, private val changePlayerReadinessUsecase: ChangePlayerReadinessUsecase, private val kickPlayerUseCase: KickPlayerUsecase, - private val leaveRoomUsecase: LeaveRoomUsecase + private val leaveRoomUsecase: LeaveRoomUsecase, + private val getRoomUsecase: GetRoomUsecase ) { @PostMapping fun createRoom( @@ -118,6 +121,17 @@ class RoomController( leaveRoomUsecase.execute(LeaveRoomUsecase.Request(roomId, jwt.subject)) } + @GetMapping("/{roomId}") + fun getRoom( + @AuthenticationPrincipal jwt: Jwt, + @PathVariable roomId: String, + ): GetRoomViewModel { + val request = GetRoomUsecase.Request(roomId, jwt.subject) + val presenter = GetRoomPresenter() + getRoomUsecase.execute(request, presenter) + return presenter.viewModel + } + class CreateRoomRequest( private val name: String, private val gameId: String, @@ -201,6 +215,7 @@ class RoomController( offset = offset ) } + } private fun GameRegistration.toView(): CreateRoomViewModel.Game = diff --git a/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/presenter/GetRoomPresenter.kt b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/presenter/GetRoomPresenter.kt new file mode 100644 index 00000000..0b6bf71f --- /dev/null +++ b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/presenter/GetRoomPresenter.kt @@ -0,0 +1,36 @@ +package tw.waterballsa.gaas.spring.controllers.presenter + +import tw.waterballsa.gaas.application.usecases.GetRoomUsecase +import tw.waterballsa.gaas.domain.GameRegistration +import tw.waterballsa.gaas.domain.Room +import tw.waterballsa.gaas.spring.controllers.viewmodel.GetRoomViewModel + +class GetRoomPresenter : GetRoomUsecase.Presenter { + lateinit var viewModel: GetRoomViewModel + private set + + override fun present(room: Room) { + viewModel = room.toViewModel() + } + +} + +private fun Room.toViewModel(): GetRoomViewModel = + GetRoomViewModel( + id = roomId!!.value, + name = name, + game = game.toViewModel(), + host = host.toViewModel(), + players = players.map { it.toViewModel() }, + maxPlayers = maxPlayers, + minPlayers = minPlayers, + currentPlayers = players.size, + isLocked = isLocked, + status = status.toString() + ) + +private fun GameRegistration.toViewModel(): GetRoomViewModel.Game = + GetRoomViewModel.Game(id!!.value, displayName) + +private fun Room.Player.toViewModel(): GetRoomViewModel.Player = + GetRoomViewModel.Player(id.value, nickname, readiness) \ No newline at end of file diff --git a/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/viewmodel/GetRoomViewModel.kt b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/viewmodel/GetRoomViewModel.kt new file mode 100644 index 00000000..94ffd4dd --- /dev/null +++ b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/viewmodel/GetRoomViewModel.kt @@ -0,0 +1,18 @@ +package tw.waterballsa.gaas.spring.controllers.viewmodel + +data class GetRoomViewModel( + val id: String, + val name: String, + val game: Game, + val host: Player, + val players: List, + val maxPlayers: Int, + val minPlayers: Int, + val currentPlayers: Int, + val isLocked: Boolean, + val status: String +) { + data class Game(val id: String, val name: String) + + data class Player(val id: String, val nickname: String, val isReady: Boolean) +} \ No newline at end of file diff --git a/spring/src/test/kotlin/tw/waterballsa/gaas/spring/it/controllers/RoomControllerTest.kt b/spring/src/test/kotlin/tw/waterballsa/gaas/spring/it/controllers/RoomControllerTest.kt index 527aeff4..4837b9dc 100644 --- a/spring/src/test/kotlin/tw/waterballsa/gaas/spring/it/controllers/RoomControllerTest.kt +++ b/spring/src/test/kotlin/tw/waterballsa/gaas/spring/it/controllers/RoomControllerTest.kt @@ -327,7 +327,7 @@ class RoomControllerTest @Autowired constructor( "winner0033", "google-oauth2|200000000000000000000" ).toRoomPlayer() - givenHostAndPlayersAreInTheRoom(host, playerB, playerC) + givenHostAndPlayersJoinedTheRoom(host, playerB, playerC) .whenUserLeaveTheRoom(userA) .thenPlayerShouldBeNotInRoomAndHostIsChanged(host) } @@ -360,6 +360,33 @@ class RoomControllerTest @Autowired constructor( .thenShouldFail("Player(${userB.id!!.value}) has joined another room.") } + @Test + fun giveHostAndPlayerBJoinedRoomC_WhenHostGetRoomC_ThenShouldGetRoomCSuccessfully() { + val userA = testUser + val host = userA.toRoomPlayer() + val playerB = createUser( + "2", "test2@mail.com", + "winner1122", "google-oauth2|100000000000000000000" + ).toRoomPlayer() + + givenHostAndPlayersJoinedTheRoom(host, playerB) + .whenUserGetTheRoom(userA) + .thenGetRoomSuccessfully() + } + + @Test + fun giveUserANotJoinedRoomB_WhenUserAGetRoomB_ThenShouldFail() { + val userA = testUser + val host = createUser( + "2", "test2@mail.com", + "winner1122", "google-oauth2|100000000000000000000" + ).toRoomPlayer() + + givenHostAndPlayersJoinedTheRoom(host) + .whenUserGetTheRoom(userA) + .thenShouldFail("Player(${userA.id!!.value}) is not in the room(${testRoom.roomId!!.value}).") + } + private fun TestGetRoomsRequest.whenUserAVisitLobby(joinUser: User): ResultActions = mockMvc.perform( get("/rooms") @@ -429,7 +456,7 @@ class RoomControllerTest @Autowired constructor( return testRoom } - private fun givenHostAndPlayersAreInTheRoom(host: Player, vararg players: Player): Room { + private fun givenHostAndPlayersJoinedTheRoom(host: Player, vararg players: Player): Room { val combinedPlayers = (listOf(host) + players).toMutableList() testRoom = createRoom(host, combinedPlayers) return testRoom @@ -451,6 +478,14 @@ class RoomControllerTest @Autowired constructor( return leaveRoom(leaveUser) } + private fun Room.whenUserGetTheRoom(user: User) = getRoom(user) + + private fun getRoom(user: User): ResultActions = + mockMvc.perform( + get("/rooms/${testRoom.roomId!!.value}") + .withJwt(user.toJwt()) + ) + private fun ResultActions.thenCreateRoomSuccessfully() { val roomView = getBody(CreateRoomViewModel::class.java) val room = roomRepository.findById(roomView.id)!! @@ -500,6 +535,33 @@ class RoomControllerTest @Autowired constructor( private fun createUser(user: User): User = userRepository.createUser(user) + private fun ResultActions.thenGetRoomSuccessfully() { + val room = roomRepository.findById(testRoom.roomId!!)!! + room.let { + andExpect(status().isOk) + .andExpect(jsonPath("$.id").exists()) + .andExpect(jsonPath("$.id").value(it.roomId!!.value)) + .andExpect(jsonPath("$.name").value(it.name)) + .andExpect(jsonPath("$.game.id").value(it.game.id!!.value)) + .andExpect(jsonPath("$.game.name").value(it.game.displayName)) + .andExpect(jsonPath("$.host.id").value(it.host.id!!.value)) + .andExpect(jsonPath("$.host.nickname").value(it.host.nickname)) + .andExpect(jsonPath("$.host.isReady").value(it.host.readiness)) + .andExpect(jsonPath("$.isLocked").value(!it.password.isNullOrEmpty())) + .andExpect(jsonPath("$.status").value(it.status.toString())) + .andExpect(jsonPath("$.currentPlayers").value(2)) + .andExpect(jsonPath("$.minPlayers").value(it.minPlayers)) + .andExpect(jsonPath("$.maxPlayers").value(it.maxPlayers)) + .andExpect(jsonPath("$.players").isArray()) + + it.players.forEachIndexed { index, player -> + andExpect(jsonPath("$.players[$index].id").value(player.id!!.value)) + .andExpect(jsonPath("$.players[$index].nickname").value(player.nickname)) + .andExpect(jsonPath("$.players[$index].isReady").value(player.readiness)) + } + } + } + private fun registerGame(): GameRegistration = gameRegistrationRepository.registerGame( GameRegistration( uniqueName = "Mahjong-python", @@ -585,9 +647,6 @@ class RoomControllerTest @Autowired constructor( private fun User.toRoomPlayer(): Player = Player(Player.Id(id!!.value), nickname) - private fun Room.hasPlayer(playerId: Player.Id): Boolean = - players.any { it.id == playerId } - private fun Room.isHost(playerId: Player.Id): Boolean = host.id == playerId }