From 52ccd33474642018e0322657711ae1c8ac6b5d7c Mon Sep 17 00:00:00 2001 From: m1a2st <100591800+m1a2st@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:56:51 +0800 Subject: [PATCH] Feature/get room list (#72) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 實作查看房間列表(#72) --- .../gaas/application/model/Pagination.kt | 8 +++ .../repositories/RoomRepository.kt | 2 + .../application/usecases/GetRoomsUseCase.kt | 29 +++++++++ .../gaas/spring/controllers/RoomController.kt | 42 ++++++++++++- .../presenter/GetRoomsPresenter.kt | 48 +++++++++++++++ .../viewmodel/GetRoomsViewModel.kt | 26 ++++++++ .../repositories/SpringRoomRepositoryImpl.kt | 14 +++++ .../gaas/spring/repositories/dao/RoomDAO.kt | 4 ++ .../it/controllers/RoomControllerTest.kt | 59 ++++++++++++++++--- .../gaas/spring/models/TestGetRoomsRequest.kt | 13 ++++ 10 files changed, 235 insertions(+), 10 deletions(-) create mode 100644 application/src/main/kotlin/tw/waterballsa/gaas/application/model/Pagination.kt create mode 100644 application/src/main/kotlin/tw/waterballsa/gaas/application/usecases/GetRoomsUseCase.kt create mode 100644 spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/presenter/GetRoomsPresenter.kt create mode 100644 spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/viewmodel/GetRoomsViewModel.kt create mode 100644 spring/src/test/kotlin/tw/waterballsa/gaas/spring/models/TestGetRoomsRequest.kt diff --git a/application/src/main/kotlin/tw/waterballsa/gaas/application/model/Pagination.kt b/application/src/main/kotlin/tw/waterballsa/gaas/application/model/Pagination.kt new file mode 100644 index 00000000..ce8f9655 --- /dev/null +++ b/application/src/main/kotlin/tw/waterballsa/gaas/application/model/Pagination.kt @@ -0,0 +1,8 @@ +package tw.waterballsa.gaas.application.model + +class Pagination( + val page: Int, + val offset: Int, + val data: List = emptyList() +) + 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 6c3fa37a..c7e40633 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 @@ -1,5 +1,6 @@ package tw.waterballsa.gaas.application.repositories +import tw.waterballsa.gaas.application.model.Pagination import tw.waterballsa.gaas.domain.Room import tw.waterballsa.gaas.domain.User @@ -9,4 +10,5 @@ interface RoomRepository { fun findById(roomId: Room.Id): Room? fun existsByHostId(hostId: User.Id): Boolean fun joinRoom(room: Room): Room + fun findByStatus(status: Room.Status, page: Pagination): Pagination } diff --git a/application/src/main/kotlin/tw/waterballsa/gaas/application/usecases/GetRoomsUseCase.kt b/application/src/main/kotlin/tw/waterballsa/gaas/application/usecases/GetRoomsUseCase.kt new file mode 100644 index 00000000..56681d00 --- /dev/null +++ b/application/src/main/kotlin/tw/waterballsa/gaas/application/usecases/GetRoomsUseCase.kt @@ -0,0 +1,29 @@ +package tw.waterballsa.gaas.application.usecases + +import tw.waterballsa.gaas.application.model.Pagination +import tw.waterballsa.gaas.application.repositories.RoomRepository +import tw.waterballsa.gaas.domain.Room +import javax.inject.Named + +@Named +class GetRoomsUseCase( + private val roomRepository: RoomRepository, +) { + + fun execute(request: Request, presenter: GetRoomsPresenter) = + roomRepository.findByStatus(request.status, request.toPagination()) + .also { presenter.present(it) } + + class Request( + val status: Room.Status, + val page: Int, + val offset: Int + ) + + interface GetRoomsPresenter { + fun present(rooms: Pagination) + } +} + +private fun GetRoomsUseCase.Request.toPagination(): Pagination = + Pagination(page, offset) 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 a0069bcb..5e3446c6 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 @@ -6,23 +6,28 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.oauth2.core.oidc.user.OidcUser import org.springframework.web.bind.annotation.* import tw.waterballsa.gaas.application.usecases.CreateRoomUsecase +import tw.waterballsa.gaas.application.usecases.GetRoomsUseCase +import tw.waterballsa.gaas.application.usecases.JoinRoomUsecase import tw.waterballsa.gaas.application.usecases.Presenter import tw.waterballsa.gaas.domain.GameRegistration import tw.waterballsa.gaas.domain.Room import tw.waterballsa.gaas.events.CreatedRoomEvent import tw.waterballsa.gaas.events.DomainEvent +import tw.waterballsa.gaas.exceptions.PlatformException import tw.waterballsa.gaas.spring.controllers.RoomController.CreateRoomViewModel +import tw.waterballsa.gaas.spring.controllers.presenter.GetRoomsPresenter +import tw.waterballsa.gaas.spring.controllers.viewmodel.GetRoomsViewModel import tw.waterballsa.gaas.spring.extensions.getEvent import javax.validation.Valid import javax.validation.constraints.Pattern -import tw.waterballsa.gaas.application.usecases.JoinRoomUsecase -import tw.waterballsa.gaas.exceptions.PlatformException +import javax.validation.constraints.Positive @RestController @RequestMapping("/rooms") class RoomController( private val createRoomUsecase: CreateRoomUsecase, - private val joinRoomUsecase: JoinRoomUsecase + private val joinRoomUsecase: JoinRoomUsecase, + private val getRoomsUseCase: GetRoomsUseCase ) { @PostMapping fun createRoom( @@ -47,6 +52,18 @@ class RoomController( return JoinRoomViewModel("success") } + @GetMapping + fun getRooms( + @RequestParam status: String, + @RequestParam page: Int, + @RequestParam offset: Int + ): GetRoomsViewModel { + val request = GetRoomsRequest(status, page, offset) + val presenter = GetRoomsPresenter() + getRoomsUseCase.execute(request.toRequest(), presenter) + return presenter.viewModel + } + class CreateRoomRequest( private val name: String, private val gameId: String, @@ -115,6 +132,25 @@ class RoomController( data class JoinRoomViewModel( val message: String ) + + class GetRoomsRequest( + @field:Pattern( + regexp = """^(WAITING|PLAYING)$""", + message = "The status must be either WAITING or PLAYING." + ) + val status: String, + @field:Positive(message = "The page must be a positive number.") + val page: Int, + @field:Positive(message = "The offset must be a positive number.") + val offset: Int + ) { + fun toRequest(): GetRoomsUseCase.Request = + GetRoomsUseCase.Request( + status = Room.Status.valueOf(status), + page = page, + offset = offset + ) + } } private fun GameRegistration.toView(): CreateRoomViewModel.Game = diff --git a/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/presenter/GetRoomsPresenter.kt b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/presenter/GetRoomsPresenter.kt new file mode 100644 index 00000000..b8726878 --- /dev/null +++ b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/presenter/GetRoomsPresenter.kt @@ -0,0 +1,48 @@ +package tw.waterballsa.gaas.spring.controllers.presenter + +import tw.waterballsa.gaas.application.model.Pagination +import tw.waterballsa.gaas.application.usecases.GetRoomsUseCase +import tw.waterballsa.gaas.domain.GameRegistration +import tw.waterballsa.gaas.domain.Room +import tw.waterballsa.gaas.spring.controllers.viewmodel.GetRoomsViewModel + +class GetRoomsPresenter : GetRoomsUseCase.GetRoomsPresenter { + lateinit var viewModel: GetRoomsViewModel + private set + + override fun present(rooms: Pagination) { + viewModel = rooms.toViewModel() + } + + private fun Pagination.toViewModel(): GetRoomsViewModel = + GetRoomsViewModel( + rooms = data.map { it.toRoomsViewModel() }, + page = toPage(data.size) + ) +} + + +private fun Room.toRoomsViewModel(): GetRoomsViewModel.RoomViewModel = + GetRoomsViewModel.RoomViewModel( + id = roomId!!.value, + name = name, + game = game.toGetRoomsView(), + host = host.toGetRoomsView(), + minPlayers = minPlayers, + maxPlayers = maxPlayers, + currentPlayers = players.size, + isLocked = isLocked, + ) + +private fun GameRegistration.toGetRoomsView(): GetRoomsViewModel.RoomViewModel.Game = + GetRoomsViewModel.RoomViewModel.Game(id!!.value, displayName) + +private fun Room.Player.toGetRoomsView(): GetRoomsViewModel.RoomViewModel.Player = + GetRoomsViewModel.RoomViewModel.Player(id.value, nickname) + +private fun Pagination.toPage(size: Int): GetRoomsViewModel.Page = + GetRoomsViewModel.Page( + page = page, + offset = offset, + total = size + ) diff --git a/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/viewmodel/GetRoomsViewModel.kt b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/viewmodel/GetRoomsViewModel.kt new file mode 100644 index 00000000..802a706b --- /dev/null +++ b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/viewmodel/GetRoomsViewModel.kt @@ -0,0 +1,26 @@ +package tw.waterballsa.gaas.spring.controllers.viewmodel + +data class GetRoomsViewModel( + val rooms: List, + val page: Page +) { + data class RoomViewModel( + val id: String, + val name: String, + val game: Game, + val host: Player, + val maxPlayers: Int, + val minPlayers: Int, + val currentPlayers: Int, + val isLocked: Boolean, + ) { + data class Game(val id: String, val name: String) + data class Player(val id: String, val nickname: String) + } + + data class Page( + val total: Int, + val page: Int, + val offset: Int + ) +} 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 6b922256..699243d1 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 @@ -1,6 +1,9 @@ package tw.waterballsa.gaas.spring.repositories +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Component +import tw.waterballsa.gaas.application.model.Pagination import tw.waterballsa.gaas.application.repositories.GameRegistrationRepository import tw.waterballsa.gaas.application.repositories.RoomRepository import tw.waterballsa.gaas.application.repositories.UserRepository @@ -33,6 +36,12 @@ class SpringRoomRepository( override fun joinRoom(room: Room): Room = roomDAO.save(room.toData()).toDomain(room.game, room.host, room.players) + override fun findByStatus(status: Room.Status, page: Pagination): Pagination { + return roomDAO.findByStatus(status, page.toPageable()) + .map { it.toDomain() } + .toPagination() + } + private fun RoomData.toDomain(): Room = Room( roomId = Id(id!!), @@ -65,3 +74,8 @@ private fun User.toRoomPlayer(): Player = id = Player.Id(id!!.value), nickname = nickname ) + +private fun Pagination.toPageable() = PageRequest.of(page, offset) + +private fun Page.toPagination(): Pagination = + Pagination(pageable.pageNumber, pageable.pageSize, content) diff --git a/spring/src/main/kotlin/tw/waterballsa/gaas/spring/repositories/dao/RoomDAO.kt b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/repositories/dao/RoomDAO.kt index cddae93c..276c1453 100644 --- a/spring/src/main/kotlin/tw/waterballsa/gaas/spring/repositories/dao/RoomDAO.kt +++ b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/repositories/dao/RoomDAO.kt @@ -1,11 +1,15 @@ package tw.waterballsa.gaas.spring.repositories.dao +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.data.mongodb.repository.MongoRepository import org.springframework.stereotype.Repository +import tw.waterballsa.gaas.domain.Room import tw.waterballsa.gaas.spring.repositories.data.RoomData @Repository interface RoomDAO : MongoRepository { fun existsByHostId(hostId: String): Boolean + fun findByStatus(status: Room.Status, pageable: Pageable): Page } 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 bb292c80..f8975fd6 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 @@ -9,9 +9,11 @@ import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser import org.springframework.security.oauth2.core.oidc.user.OidcUser 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.result.MockMvcResultMatchers.jsonPath import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import tw.waterballsa.gaas.application.model.Pagination import tw.waterballsa.gaas.application.repositories.GameRegistrationRepository import tw.waterballsa.gaas.application.repositories.RoomRepository import tw.waterballsa.gaas.application.repositories.UserRepository @@ -21,6 +23,7 @@ import tw.waterballsa.gaas.domain.Room.Player import tw.waterballsa.gaas.domain.User import tw.waterballsa.gaas.spring.it.AbstractSpringBootTest import tw.waterballsa.gaas.spring.models.TestCreateRoomRequest +import tw.waterballsa.gaas.spring.models.TestGetRoomsRequest import tw.waterballsa.gaas.spring.models.TestJoinRoomRequest import java.time.Instant.now @@ -81,7 +84,6 @@ class RoomControllerTest @Autowired constructor( val request = createRoomRequest("1234") createRoom(request) .thenCreateRoomSuccessfully(request) - createRoom(request) .andExpect(status().isBadRequest) .andExpect(jsonPath("$.message").value("A user can only create one room at a time.")) @@ -117,6 +119,51 @@ class RoomControllerTest @Autowired constructor( .thenJoinRoomSuccessfully() } + @Test + fun givenWaitingRoomBAndWaitingRoomC_WhenUserAVisitLobby_ThenShouldHaveRoomBAndRoomC() { + val userA = testUser + val userB = createUser("2", "test2@mail.com", "winner1122") + val userC = createUser("3", "test3@mail.com", "winner1234") + val request = TestGetRoomsRequest("WAITING", 0, 10) + + givenWaitingRooms(userB, userC) + request.whenUserAVisitLobby(userA) + .thenShouldHaveRooms(request) + } + + private fun TestGetRoomsRequest.whenUserAVisitLobby(joinUser: User): ResultActions = + mockMvc.perform( + get("/rooms") + .with(oidcLogin().oidcUser(mockOidcUser(joinUser))) + .param("status", status) + .param("page", page.toString()) + .param("offset", offset.toString()) + ) + + private fun givenWaitingRooms(vararg users: User) = + users.forEach { givenTheHostCreatePublicRoom(it) } + + private fun ResultActions.thenShouldHaveRooms(request: TestGetRoomsRequest) { + val rooms = roomRepository.findByStatus(request.toStatus(), request.toPagination()) + andExpect(status().isOk) + .andExpect(jsonPath("$.rooms").isArray) + .andExpect(jsonPath("$.rooms.length()").value(rooms.data.size)) + .roomExcept(rooms) + } + + private fun ResultActions.roomExcept(rooms: Pagination) { + rooms.data.forEachIndexed() { index, room -> + andExpect(jsonPath("$.rooms[$index].id").value(room.roomId!!.value)) + .andExpect(jsonPath("$.rooms[$index].name").value(room.name)) + .andExpect(jsonPath("$.rooms[$index].game.id").value(room.game.id!!.value)) + .andExpect(jsonPath("$.rooms[$index].host.id").value(room.host.id.value)) + .andExpect(jsonPath("$.rooms[$index].isLocked").value(room.isLocked)) + .andExpect(jsonPath("$.rooms[$index].currentPlayers").value(room.players.size)) + .andExpect(jsonPath("$.rooms[$index].maxPlayers").value(room.maxPlayers)) + .andExpect(jsonPath("$.rooms[$index].minPlayers").value(room.minPlayers)) + } + } + private fun createRoom(request: TestCreateRoomRequest): ResultActions = mockMvc.perform( post("/rooms") @@ -133,17 +180,16 @@ class RoomControllerTest @Autowired constructor( private fun givenTheHostCreatePublicRoom(host: User): Room { testRoom = createRoom(host) - return testRoom + return testRoom } private fun givenTheHostCreateRoomWithPassword(host: User, password: String): Room { testRoom = createRoom(host, password) return testRoom - } - private fun Room.whenUserJoinTheRoom(user: User, password: String? = null):ResultActions{ - val request = joinRoomRequest(password); + private fun Room.whenUserJoinTheRoom(user: User, password: String? = null): ResultActions { + val request = joinRoomRequest(password) val joinUser = mockOidcUser(user) return joinRoom(request, joinUser) } @@ -191,7 +237,7 @@ class RoomControllerTest @Autowired constructor( ) ) - private fun createRoom(host : User, password: String? = null): Room = roomRepository.createRoom( + private fun createRoom(host: User, password: String? = null): Room = roomRepository.createRoom( Room( game = testGame, host = Player(Player.Id(host.id!!.value), host.nickname), @@ -229,5 +275,4 @@ class RoomControllerTest @Autowired constructor( TestJoinRoomRequest( password = password ) - } diff --git a/spring/src/test/kotlin/tw/waterballsa/gaas/spring/models/TestGetRoomsRequest.kt b/spring/src/test/kotlin/tw/waterballsa/gaas/spring/models/TestGetRoomsRequest.kt new file mode 100644 index 00000000..9dc879ee --- /dev/null +++ b/spring/src/test/kotlin/tw/waterballsa/gaas/spring/models/TestGetRoomsRequest.kt @@ -0,0 +1,13 @@ +package tw.waterballsa.gaas.spring.models + +import tw.waterballsa.gaas.application.model.Pagination +import tw.waterballsa.gaas.domain.Room + +class TestGetRoomsRequest( + val status: String, + val page: Int, + val offset: Int +) { + fun toStatus() : Room.Status = Room.Status.valueOf(status) + fun toPagination(): Pagination = Pagination(page, offset) +}