diff --git a/application/src/main/kotlin/tw/waterballsa/gaas/application/repositories/RoomRepository.kt b/application/src/main/kotlin/tw/waterballsa/gaas/application/repositories/RoomRepository.kt index abf1c15a..e3abd6f4 100644 --- a/application/src/main/kotlin/tw/waterballsa/gaas/application/repositories/RoomRepository.kt +++ b/application/src/main/kotlin/tw/waterballsa/gaas/application/repositories/RoomRepository.kt @@ -12,4 +12,5 @@ interface RoomRepository { fun update(room: Room): Room fun findByStatus(status: Room.Status, page: Pagination): Pagination fun deleteById(roomId: Room.Id) + fun leaveRoom(room: Room) } diff --git a/application/src/main/kotlin/tw/waterballsa/gaas/application/usecases/LeaveRoomUsecase.kt b/application/src/main/kotlin/tw/waterballsa/gaas/application/usecases/LeaveRoomUsecase.kt new file mode 100644 index 00000000..83ecbde3 --- /dev/null +++ b/application/src/main/kotlin/tw/waterballsa/gaas/application/usecases/LeaveRoomUsecase.kt @@ -0,0 +1,33 @@ +package tw.waterballsa.gaas.application.usecases + +import tw.waterballsa.gaas.application.eventbus.EventBus +import tw.waterballsa.gaas.application.repositories.RoomRepository +import tw.waterballsa.gaas.domain.Room +import tw.waterballsa.gaas.domain.Room.Player +import tw.waterballsa.gaas.exceptions.NotFoundException.Companion.notFound +import javax.inject.Named + +@Named +class LeaveRoomUsecase( + private val roomRepository: RoomRepository, + private val eventBus: EventBus +) { + + fun execute(request: LeaveRoomUsecase.Request) { + with(request) { + val room = findRoomById(Room.Id(roomId)) + room.leaveRoom(Player.Id(playerId)) + roomRepository.leaveRoom(room) + } + } + + private fun findRoomById(roomId: Room.Id) = + roomRepository.findById(roomId) + ?: throw notFound(Room::class).id(roomId) + + data class Request( + val roomId: String, + val playerId: String + ) + +} \ 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 dbfb9a8e..6f092d0c 100644 --- a/domain/src/main/kotlin/tw/waterballsa/gaas/domain/Room.kt +++ b/domain/src/main/kotlin/tw/waterballsa/gaas/domain/Room.kt @@ -6,7 +6,7 @@ import tw.waterballsa.gaas.exceptions.PlatformException class Room( var roomId: Id? = null, val game: GameRegistration, - val host: Player, + var host: Player, val players: MutableList, val maxPlayers: Int, val minPlayers: Int, @@ -44,6 +44,18 @@ class Room( players.remove(player) } + fun leaveRoom(playerId: Player.Id) { + players.removeIf { it.id == playerId } + if (playerId == host.id) { + changeHost() + } + } + + private fun changeHost() { + players.firstOrNull() + ?.let { host = it } + } + private fun findPlayer(playerId: Player.Id): Player? = players.find { it.id == playerId } @JvmInline 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 aa72daec..bcd98893 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 @@ -29,7 +29,8 @@ class RoomController( private val getRoomsUseCase: GetRoomsUseCase, private val closeRoomsUseCase: CloseRoomUsecase, private val changePlayerReadinessUsecase: ChangePlayerReadinessUsecase, - private val kickPlayerUseCase: KickPlayerUsecase + private val kickPlayerUseCase: KickPlayerUsecase, + private val leaveRoomUsecase: LeaveRoomUsecase ) { @PostMapping fun createRoom( @@ -112,6 +113,16 @@ class RoomController( return PlatformViewModel.success() } + @DeleteMapping("/{roomId}/players/me") + @ResponseStatus(NO_CONTENT) + fun leaveRoom( + @AuthenticationPrincipal jwt: Jwt, + @PathVariable roomId: String + ) { + val leaverId = jwt.subject ?: throw PlatformException("User id must exist.") + leaveRoomUsecase.execute(LeaveRoomUsecase.Request(roomId, leaverId)) + } + class CreateRoomRequest( private val name: String, private val gameId: String, diff --git a/spring/src/main/kotlin/tw/waterballsa/gaas/spring/repositories/SpringRoomRepositoryImpl.kt b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/repositories/SpringRoomRepositoryImpl.kt index 67b5ef8b..22871c63 100644 --- a/spring/src/main/kotlin/tw/waterballsa/gaas/spring/repositories/SpringRoomRepositoryImpl.kt +++ b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/repositories/SpringRoomRepositoryImpl.kt @@ -42,6 +42,10 @@ class SpringRoomRepository( roomDAO.deleteById(roomId.value) } + override fun leaveRoom(room: Room) { + roomDAO.save(room.toData()) + } + private fun RoomData.toDomain(): Room = Room( roomId = Id(id!!), 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 f4996f00..50cf8d45 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 @@ -1,18 +1,18 @@ package tw.waterballsa.gaas.spring.it.controllers -import org.assertj.core.api.Assertions.* +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.security.oauth2.core.oidc.OidcIdToken import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser import org.springframework.security.oauth2.core.oidc.user.OidcUser +import org.springframework.security.oauth2.jwt.Jwt import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin import org.springframework.test.web.servlet.ResultActions -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import tw.waterballsa.gaas.application.model.Pagination @@ -272,6 +272,18 @@ class RoomControllerTest @Autowired constructor( .thenPlayersShouldBeInTheRoom(roomC.roomId!!, hostA, playerB) } + @Test + fun givenHostAndPlayerBAndPlayerCAreInRoomD_WhenHostLeaveRoomD_ThenPreviousHostShouldBeNotInRoomDAndChangedNewHost() { + val userA = testUser + val host = userA.toRoomPlayer() + val playerB = createUser("2", "test2@mail.com", "winner1122").toRoomPlayer() + val playerC = createUser("3", "test3@mail.com", "winner0033").toRoomPlayer() + + givenHostAndPlayersAreInTheRoom(host, playerB, playerC) + .whenUserLeaveTheRoom(userA) + .thenPlayerShouldBeNotInRoomAndHostIsChanged(host) + } + private fun TestGetRoomsRequest.whenUserAVisitLobby(joinUser: User): ResultActions = mockMvc.perform( get("/rooms") @@ -325,6 +337,12 @@ class RoomControllerTest @Autowired constructor( .withJwt(user.id!!.value.toJwt()) ) + private fun leaveRoom(leaveUser: Jwt): ResultActions = + mockMvc.perform( + delete("/rooms/${testRoom.roomId!!.value}/players/me") + .withJwt(leaveUser) + ) + private fun givenTheHostCreatePublicRoom(host: User): Room { testRoom = createRoom(host) return testRoom @@ -335,6 +353,12 @@ class RoomControllerTest @Autowired constructor( return testRoom } + private fun givenHostAndPlayersAreInTheRoom(host: Player, vararg players: Player): Room { + val combinedPlayers = (listOf(host) + players).toMutableList() + testRoom = createRoom(host, combinedPlayers) + return testRoom + } + private fun Room.whenUserJoinTheRoom(user: User, password: String? = null): ResultActions { val request = joinRoomRequest(password) val joinUser = mockOidcUser(user) @@ -349,6 +373,11 @@ class RoomControllerTest @Autowired constructor( post("/rooms/${roomId.value}/players/me:cancel").withJwt(user.id!!.value.toJwt()) ) + private fun Room.whenUserLeaveTheRoom(user: User): ResultActions { + val leaveUser = user.id!!.value.toJwt() + return leaveRoom(leaveUser) + } + private fun ResultActions.thenCreateRoomSuccessfully(request: TestCreateRoomRequest) { request.let { andExpect(status().isCreated) @@ -380,6 +409,13 @@ class RoomControllerTest @Autowired constructor( .andExpect(jsonPath("$.message").value("${resourceType.simpleName} not found")) } + private fun ResultActions.thenPlayerShouldBeNotInRoomAndHostIsChanged(player: Player) { + andExpect(status().isNoContent) + val room = roomRepository.findById(testRoom.roomId!!)!! + assertFalse(room.hasPlayer(player.id)) + assertFalse(room.isHost(player.id)) + } + private fun createUser(id: String, email: String, nickname: String): User = userRepository.createUser(User(User.Id(id), email, nickname)) @@ -411,6 +447,20 @@ class RoomControllerTest @Autowired constructor( ) ) + private fun createRoom(host: Player, players: MutableList, password: String? = null): Room = + roomRepository.createRoom( + Room( + game = testGame, + host = host, + players = players, + maxPlayers = testGame.maxPlayers, + minPlayers = testGame.minPlayers, + name = "My Room", + status = Room.Status.WAITING, + password = password + ) + ) + private fun createRoomRequest(password: String? = null): TestCreateRoomRequest = TestCreateRoomRequest( name = "Rapid Mahjong Room", @@ -466,4 +516,13 @@ class RoomControllerTest @Autowired constructor( findRoomById(roomId)!!.players.map { it.id.value }.let { assertThat(it).containsAll(users.map { user -> user.id!!.value }) } + + 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 }