From 4e807620efb2ca34a482c8c09c7bf6d0eee4b2cc Mon Sep 17 00:00:00 2001 From: Bob Sin Date: Tue, 28 May 2024 00:40:21 +0900 Subject: [PATCH] =?UTF-8?q?[KAN-104]=20=EC=9D=8C=EC=8B=9D=EC=A0=90=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20=EC=A2=8B=EC=95=84=EC=9A=94=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C,=20=EC=A2=8B=EC=95=84=EC=9A=94=20=EC=9A=94=EC=B2=AD/?= =?UTF-8?q?=EC=B7=A8=EC=86=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20+=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/LikeRestaurantService.kt | 23 +- .../RestaurantRepositoryCustomImpl.kt | 4 +- .../controller/GetRestaurantControllerTest.kt | 2 +- .../LikeRestaurantControllerTest.kt | 470 ++++++++++++++++++ 4 files changed, 486 insertions(+), 13 deletions(-) create mode 100644 src/test/kotlin/com/restaurant/be/restaurant/presentation/controller/LikeRestaurantControllerTest.kt diff --git a/src/main/kotlin/com/restaurant/be/restaurant/domain/service/LikeRestaurantService.kt b/src/main/kotlin/com/restaurant/be/restaurant/domain/service/LikeRestaurantService.kt index a4770fe..3fc5250 100644 --- a/src/main/kotlin/com/restaurant/be/restaurant/domain/service/LikeRestaurantService.kt +++ b/src/main/kotlin/com/restaurant/be/restaurant/domain/service/LikeRestaurantService.kt @@ -8,8 +8,6 @@ import com.restaurant.be.restaurant.presentation.controller.dto.GetLikeRestauran import com.restaurant.be.restaurant.presentation.controller.dto.LikeRestaurantResponse import com.restaurant.be.restaurant.repository.RestaurantLikeRepository import com.restaurant.be.restaurant.repository.RestaurantRepository -import com.restaurant.be.restaurant.repository.dto.RestaurantProjectionDto -import com.restaurant.be.user.domain.entity.User import com.restaurant.be.user.repository.UserRepository import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service @@ -23,31 +21,36 @@ class LikeRestaurantService( ) { @Transactional fun likeRestaurant(email: String, restaurantId: Long, isLike: Boolean): LikeRestaurantResponse { - val user: User = userRepository.findByEmail(email) ?: throw NotFoundUserEmailException() - val userId: Long = user.id ?: throw NotFoundUserException() + val userId: Long = userRepository.findByEmail(email)?.id ?: throw NotFoundUserException() - val restaurant: RestaurantProjectionDto = restaurantRepository.findDtoById(restaurantId) + val restaurantDto = restaurantRepository.findDtoById(restaurantId) ?: throw NotFoundRestaurantException() - // 좋아요 요청 + val restaurant = restaurantRepository.findById(restaurantId) + .orElseThrow { NotFoundRestaurantException() } + if (isLike) { - // 실제 좋아요가 아닐 시 Insert - if (!restaurant.isLike) { + if (!restaurantDto.isLike) { restaurantLikeRepository.save( RestaurantLike( restaurantId = restaurantId, userId = userId ) ) + restaurant.likeCount += 1 } } else { - restaurantLikeRepository.deleteByUserIdAndRestaurantId(userId, restaurantId) + if (restaurantDto.isLike) { + restaurant.likeCount -= 1 + restaurantLikeRepository.deleteByUserIdAndRestaurantId(userId, restaurantId) + } } + restaurantRepository.save(restaurant) return LikeRestaurantResponse(restaurantRepository.findDtoById(restaurantId)!!.toDto()) } - @Transactional + @Transactional(readOnly = true) fun getMyLikeRestaurant(pageable: Pageable, email: String): GetLikeRestaurantsResponse { val userId = userRepository.findByEmail(email)?.id ?: throw NotFoundUserEmailException() diff --git a/src/main/kotlin/com/restaurant/be/restaurant/repository/RestaurantRepositoryCustomImpl.kt b/src/main/kotlin/com/restaurant/be/restaurant/repository/RestaurantRepositoryCustomImpl.kt index c3a5027..f2de473 100644 --- a/src/main/kotlin/com/restaurant/be/restaurant/repository/RestaurantRepositoryCustomImpl.kt +++ b/src/main/kotlin/com/restaurant/be/restaurant/repository/RestaurantRepositoryCustomImpl.kt @@ -145,8 +145,6 @@ class RestaurantRepositoryCustomImpl( val total = myLikeQuery.fetchCount() val restaurantIds = myLikeQuery - .offset(pageable.offset) - .limit(pageable.pageSize.toLong()) .fetch() val restaurantInfos = queryFactory @@ -156,6 +154,8 @@ class RestaurantRepositoryCustomImpl( .leftJoin(restaurantLike).on(restaurant.id.eq(restaurantLike.restaurantId)) .orderBy(*orderSpecifier.toTypedArray()) .fetchJoin() + .offset(pageable.offset) + .limit(pageable.pageSize.toLong()) .fetch() val menus = queryFactory diff --git a/src/test/kotlin/com/restaurant/be/restaurant/presentation/controller/GetRestaurantControllerTest.kt b/src/test/kotlin/com/restaurant/be/restaurant/presentation/controller/GetRestaurantControllerTest.kt index cee7823..f0671d4 100644 --- a/src/test/kotlin/com/restaurant/be/restaurant/presentation/controller/GetRestaurantControllerTest.kt +++ b/src/test/kotlin/com/restaurant/be/restaurant/presentation/controller/GetRestaurantControllerTest.kt @@ -1737,7 +1737,7 @@ class GetRestaurantControllerTest( actualResult.data!!.restaurants.content[0].name shouldBe "목구멍 율전점1" } - it("when 2 data and set size 1 page 2 should return 2's restaurant") { + it("when 2 data and set size 1 page 1 should return 2's restaurant") { // given val restaurantEntity1 = RestaurantUtil.generateRestaurantEntity( name = "목구멍 율전점1" diff --git a/src/test/kotlin/com/restaurant/be/restaurant/presentation/controller/LikeRestaurantControllerTest.kt b/src/test/kotlin/com/restaurant/be/restaurant/presentation/controller/LikeRestaurantControllerTest.kt new file mode 100644 index 0000000..b07bd0c --- /dev/null +++ b/src/test/kotlin/com/restaurant/be/restaurant/presentation/controller/LikeRestaurantControllerTest.kt @@ -0,0 +1,470 @@ +package com.restaurant.be.restaurant.presentation.controller + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.restaurant.be.common.CustomDescribeSpec +import com.restaurant.be.common.IntegrationTest +import com.restaurant.be.common.PageDeserializer +import com.restaurant.be.common.response.CommonResponse +import com.restaurant.be.common.util.RestaurantUtil +import com.restaurant.be.common.util.setUpUser +import com.restaurant.be.restaurant.domain.entity.RestaurantLike +import com.restaurant.be.restaurant.presentation.controller.dto.GetRestaurantsResponse +import com.restaurant.be.restaurant.presentation.controller.dto.LikeRestaurantResponse +import com.restaurant.be.restaurant.presentation.controller.dto.common.RestaurantDto +import com.restaurant.be.restaurant.repository.RestaurantLikeRepository +import com.restaurant.be.restaurant.repository.RestaurantRepository +import com.restaurant.be.user.domain.entity.User +import com.restaurant.be.user.repository.UserRepository +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import org.springframework.data.domain.Page +import org.springframework.test.web.servlet.MockMvc +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 org.springframework.transaction.annotation.Transactional +import java.nio.charset.Charset + +@IntegrationTest +@Transactional +class LikeRestaurantControllerTest( + private val mockMvc: MockMvc, + private val userRepository: UserRepository, + private val restaurantLikeRepository: RestaurantLikeRepository, + private val restaurantRepository: RestaurantRepository +) : CustomDescribeSpec() { + private val baseUrl = "/v1/restaurants" + private val objectMapper: ObjectMapper = ObjectMapper().registerModule(KotlinModule()).apply { + val module = SimpleModule() + module.addDeserializer(Page::class.java, PageDeserializer(RestaurantDto::class.java)) + this.registerModule(module) + } + + init { + beforeEach { + setUpUser("test@gmail.com", userRepository) + } + + describe("#getMyLikeRestaurants basic test") { + it("when no saved should return empty") { + // given + // when + val result = mockMvc.perform( + get("$baseUrl/my-like") + ).also { + println(it.andReturn().response.contentAsString) + } + .andExpect(status().isOk) + .andExpect(jsonPath("$.result").value("SUCCESS")) + .andReturn() + + val responseContent = result.response.getContentAsString(Charset.forName("UTF-8")) + val responseType = + object : TypeReference>() {} + val actualResult: CommonResponse = objectMapper.readValue( + responseContent, + responseType + ) + + // then + actualResult.data!!.restaurants.content.size shouldBe 0 + } + + it("when user's like saved should return saved like") { + // given + val newUser = userRepository.findByEmail("test@gmail.com") + val restaurantEntity = RestaurantUtil.generateRestaurantEntity( + name = "목구멍 율전점" + ) + restaurantRepository.save(restaurantEntity) + restaurantLikeRepository.save( + RestaurantLike( + userId = newUser?.id ?: 0, + restaurantId = restaurantEntity.id + ) + ) + + // when + val result = mockMvc.perform( + get("$baseUrl/my-like") + ).also { + println(it.andReturn().response.contentAsString) + } + .andExpect(status().isOk) + .andExpect(jsonPath("$.result").value("SUCCESS")) + .andReturn() + + val responseContent = result.response.getContentAsString(Charset.forName("UTF-8")) + val responseType = + object : TypeReference>() {} + val actualResult: CommonResponse = objectMapper.readValue( + responseContent, + responseType + ) + + // then + actualResult.data!!.restaurants.content.size shouldBe 1 + actualResult.data!!.restaurants.content[0].name shouldBe "목구멍 율전점" + actualResult.data!!.restaurants.content[0].isLike shouldBe true + } + + it("when another user's like saved should return empty") { + // given + val anotherUser = userRepository.save( + User( + id = 2, + email = "test2@gmail.com", + nickname = "test2", + profileImageUrl = "" + ) + ) + val restaurantEntity = RestaurantUtil.generateRestaurantEntity( + name = "목구멍 율전점" + ) + restaurantRepository.save(restaurantEntity) + restaurantLikeRepository.save( + RestaurantLike( + userId = anotherUser.id ?: 0, + restaurantId = restaurantEntity.id + ) + ) + + // when + val result = mockMvc.perform( + get("$baseUrl/my-like") + ).also { + println(it.andReturn().response.contentAsString) + } + .andExpect(status().isOk) + .andExpect(jsonPath("$.result").value("SUCCESS")) + .andReturn() + + val responseContent = result.response.getContentAsString(Charset.forName("UTF-8")) + val responseType = + object : TypeReference>() {} + val actualResult: CommonResponse = objectMapper.readValue( + responseContent, + responseType + ) + + // then + actualResult.data!!.restaurants.content.size shouldBe 0 + } + } + + describe("#getMyLikeRestaurants pagination test") { + it("when 1 data and set size 1 should return 1 data") { + // given + val newUser = userRepository.findByEmail("test@gmail.com") + val restaurantEntity = RestaurantUtil.generateRestaurantEntity( + name = "목구멍 율전점" + ) + restaurantRepository.save(restaurantEntity) + restaurantLikeRepository.save( + RestaurantLike( + userId = newUser?.id ?: 0, + restaurantId = restaurantEntity.id + ) + ) + + // when + val result = mockMvc.perform( + get("$baseUrl/my-like?page=0&size=1") + ).also { + println(it.andReturn().response.contentAsString) + } + .andExpect(status().isOk) + .andExpect(jsonPath("$.result").value("SUCCESS")) + .andReturn() + + val responseContent = result.response.getContentAsString(Charset.forName("UTF-8")) + val responseType = + object : TypeReference>() {} + val actualResult: CommonResponse = objectMapper.readValue( + responseContent, + responseType + ) + + // then + actualResult.data!!.restaurants.content.size shouldBe 1 + actualResult.data!!.restaurants.content[0].name shouldBe "목구멍 율전점" + actualResult.data!!.restaurants.content[0].isLike shouldBe true + } + + it("when 2 data and set size 1 should return 1 data") { + // given + val newUser = userRepository.findByEmail("test@gmail.com") + val restaurantEntity1 = RestaurantUtil.generateRestaurantEntity( + name = "목구멍 율전점" + ) + val restaurantEntity2 = RestaurantUtil.generateRestaurantEntity( + name = "목구멍 율전점2" + ) + restaurantRepository.save(restaurantEntity1) + restaurantRepository.save(restaurantEntity2) + restaurantLikeRepository.save( + RestaurantLike( + userId = newUser?.id ?: 0, + restaurantId = restaurantEntity1.id + ) + ) + restaurantLikeRepository.save( + RestaurantLike( + userId = newUser?.id ?: 0, + restaurantId = restaurantEntity2.id + ) + ) + + // when + val result = mockMvc.perform( + get("$baseUrl/my-like") + .param("page", "0") + .param("size", "1") + ).also { + println(it.andReturn().response.contentAsString) + } + .andExpect(status().isOk) + .andExpect(jsonPath("$.result").value("SUCCESS")) + .andReturn() + + val responseContent = result.response.getContentAsString(Charset.forName("UTF-8")) + val responseType = + object : TypeReference>() {} + val actualResult: CommonResponse = objectMapper.readValue( + responseContent, + responseType + ) + + // then + actualResult.data!!.restaurants.content.size shouldBe 1 + actualResult.data!!.restaurants.content[0].name shouldBe "목구멍 율전점2" + actualResult.data!!.restaurants.content[0].isLike shouldBe true + } + + it("when 2 data and set size 1 page 0 should return 1's restaurant") { + // given + val newUser = userRepository.findByEmail("test@gmail.com") + val restaurantEntity1 = RestaurantUtil.generateRestaurantEntity( + name = "목구멍 율전점" + ) + val restaurantEntity2 = RestaurantUtil.generateRestaurantEntity( + name = "목구멍 율전점2" + ) + restaurantRepository.save(restaurantEntity1) + restaurantRepository.save(restaurantEntity2) + restaurantLikeRepository.save( + RestaurantLike( + userId = newUser?.id ?: 0, + restaurantId = restaurantEntity1.id + ) + ) + restaurantLikeRepository.save( + RestaurantLike( + userId = newUser?.id ?: 0, + restaurantId = restaurantEntity2.id + ) + ) + + // when + val result = mockMvc.perform( + get("$baseUrl/my-like") + .param("page", "0") + .param("size", "1") + ).also { + println(it.andReturn().response.contentAsString) + } + .andExpect(status().isOk) + .andExpect(jsonPath("$.result").value("SUCCESS")) + .andReturn() + + val responseContent = result.response.getContentAsString(Charset.forName("UTF-8")) + val responseType = + object : TypeReference>() {} + val actualResult: CommonResponse = objectMapper.readValue( + responseContent, + responseType + ) + + // then + actualResult.data!!.restaurants.content.size shouldBe 1 + actualResult.data!!.restaurants.content[0].name shouldBe "목구멍 율전점2" + actualResult.data!!.restaurants.content[0].isLike shouldBe true + } + + it("when 2 data and set size 1 page 1 should return 2's restaurant") { + // given + val newUser = userRepository.findByEmail("test@gmail.com") + val restaurantEntity1 = RestaurantUtil.generateRestaurantEntity( + name = "목구멍 율전점" + ) + val restaurantEntity2 = RestaurantUtil.generateRestaurantEntity( + name = "목구멍 율전점2" + ) + restaurantRepository.save(restaurantEntity1) + restaurantRepository.save(restaurantEntity2) + restaurantLikeRepository.save( + RestaurantLike( + userId = newUser?.id ?: 0, + restaurantId = restaurantEntity1.id + ) + ) + restaurantLikeRepository.save( + RestaurantLike( + userId = newUser?.id ?: 0, + restaurantId = restaurantEntity2.id + ) + ) + + // when + val result = mockMvc.perform( + get("$baseUrl/my-like") + .param("page", "1") + .param("size", "1") + ).also { + println(it.andReturn().response.contentAsString) + } + .andExpect(status().isOk) + .andExpect(jsonPath("$.result").value("SUCCESS")) + .andReturn() + + val responseContent = result.response.getContentAsString(Charset.forName("UTF-8")) + val responseType = + object : TypeReference>() {} + val actualResult: CommonResponse = objectMapper.readValue( + responseContent, + responseType + ) + + // then + actualResult.data!!.restaurants.content.size shouldBe 1 + actualResult.data!!.restaurants.content[0].name shouldBe "목구멍 율전점" + actualResult.data!!.restaurants.content[0].isLike shouldBe true + } + } + + describe("#likeRestaurant basic test") { + it("when like restaurant should success like") { + // given + val restaurantEntity = RestaurantUtil.generateRestaurantEntity( + name = "목구멍 율전점" + ) + restaurantRepository.save(restaurantEntity) + + // when + val result = mockMvc.perform( + post("$baseUrl/${restaurantEntity.id}/like") + .content( + objectMapper.writeValueAsString( + mapOf( + "isLike" to true + ) + ) + ) + .contentType("application/json") + ).also { + println(it.andReturn().response.contentAsString) + } + .andExpect(status().isOk) + .andExpect(jsonPath("$.result").value("SUCCESS")) + .andReturn() + + val responseContent = result.response.getContentAsString(Charset.forName("UTF-8")) + val responseType = + object : TypeReference>() {} + val actualResult: CommonResponse = objectMapper.readValue( + responseContent, + responseType + ) + + // then + actualResult.data!!.restaurant.shouldNotBeNull() + actualResult.data!!.restaurant.name shouldBe "목구멍 율전점" + actualResult.data!!.restaurant.isLike shouldBe true + actualResult.data!!.restaurant.likeCount shouldBe 1 + } + + it("when unlike restaurant should success unlike") { + // given + val restaurantEntity = RestaurantUtil.generateRestaurantEntity( + name = "목구멍 율전점" + ) + restaurantRepository.save(restaurantEntity) + val newUser = userRepository.findByEmail("test@gmail.com") + restaurantLikeRepository.save( + RestaurantLike( + userId = newUser?.id ?: 0, + restaurantId = restaurantEntity.id + ) + ) + + // when + val result = mockMvc.perform( + post("$baseUrl/${restaurantEntity.id}/like") + .content( + objectMapper.writeValueAsString( + mapOf( + "isLike" to false + ) + ) + ) + .contentType("application/json") + ).also { + println(it.andReturn().response.contentAsString) + } + .andExpect(status().isOk) + .andExpect(jsonPath("$.result").value("SUCCESS")) + .andReturn() + + val responseContent = result.response.getContentAsString(Charset.forName("UTF-8")) + val responseType = + object : TypeReference>() {} + val actualResult: CommonResponse = objectMapper.readValue( + responseContent, + responseType + ) + + // then + actualResult.data!!.restaurant.shouldNotBeNull() + actualResult.data!!.restaurant.name shouldBe "목구멍 율전점" + actualResult.data!!.restaurant.isLike shouldBe false + actualResult.data!!.restaurant.likeCount shouldBe -1 + } + + it("when not exist restaurant should return not found") { + // given + // when + val result = mockMvc.perform( + post("$baseUrl/1/like") + .content( + objectMapper.writeValueAsString( + mapOf( + "isLike" to true + ) + ) + ) + .contentType("application/json") + ).also { + println(it.andReturn().response.contentAsString) + } + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.result").value("FAIL")) + .andReturn() + + val responseContent = result.response.getContentAsString(Charset.forName("UTF-8")) + val responseType = + object : TypeReference>() {} + val actualResult: CommonResponse = objectMapper.readValue( + responseContent, + responseType + ) + + // then + actualResult.message shouldBe "해당 식당 정보가 존재하지 않습니다." + } + } + } +}