diff --git a/shop-backend/build.gradle.kts b/shop-backend/build.gradle.kts index ef0747d..7490ca3 100644 --- a/shop-backend/build.gradle.kts +++ b/shop-backend/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { runtimeOnly("com.h2database:h2") runtimeOnly("org.postgresql:postgresql") testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.mockito:mockito-core") } tasks.withType { diff --git a/shop-backend/src/main/kotlin/no/nav/shopbackend/controller/ProductController.kt b/shop-backend/src/main/kotlin/no/nav/shopbackend/controller/ProductController.kt index bfafecf..321e2ed 100644 --- a/shop-backend/src/main/kotlin/no/nav/shopbackend/controller/ProductController.kt +++ b/shop-backend/src/main/kotlin/no/nav/shopbackend/controller/ProductController.kt @@ -76,9 +76,13 @@ class ProductController { } @GetMapping("/{id:\\d+}/ratings") - fun findRatingsByProductId(@PathVariable id: Long): ResponseEntity> { - val product = productRepository.findById(id) - return product.map { ResponseEntity.ok(it.ratings) }.orElse(ResponseEntity.notFound().build()) + fun findRatingsByProductId(@PathVariable id: Long): ResponseEntity> { + val ratings = ratingRepository.findByProductId(id) + return if (ratings.isNotEmpty()) { + ResponseEntity.ok(ratings) + } else { + ResponseEntity.notFound().build() + } } @PostMapping("/{id:\\d+}/ratings") diff --git a/shop-backend/src/main/kotlin/no/nav/shopbackend/model/Rating.kt b/shop-backend/src/main/kotlin/no/nav/shopbackend/model/Rating.kt index ed1700b..f786cb7 100644 --- a/shop-backend/src/main/kotlin/no/nav/shopbackend/model/Rating.kt +++ b/shop-backend/src/main/kotlin/no/nav/shopbackend/model/Rating.kt @@ -1,5 +1,6 @@ package no.nav.shopbackend.model +import com.fasterxml.jackson.annotation.JsonIgnore import jakarta.persistence.Entity import jakarta.persistence.FetchType import jakarta.persistence.GeneratedValue @@ -14,7 +15,10 @@ data class Rating( @field:NotNull val stars: Int, val comment: String?, val sentiment: String?, - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id") val product: Product?, + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + val product: Product?, @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long = -1 ) { private constructor() : this(-1, "", "", null, -1L) diff --git a/shop-backend/src/main/kotlin/no/nav/shopbackend/repo/RatingRepository.kt b/shop-backend/src/main/kotlin/no/nav/shopbackend/repo/RatingRepository.kt index 475af6c..f72068b 100644 --- a/shop-backend/src/main/kotlin/no/nav/shopbackend/repo/RatingRepository.kt +++ b/shop-backend/src/main/kotlin/no/nav/shopbackend/repo/RatingRepository.kt @@ -2,10 +2,15 @@ package no.nav.shopbackend.repo import no.nav.shopbackend.model.Rating import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.CrudRepository +import org.springframework.data.repository.query.Param import org.springframework.stereotype.Repository @Repository interface RatingRepository : CrudRepository { fun findAll(pageable: Pageable): Iterable + + @Query("SELECT r FROM Rating r WHERE r.product.id = :productId") + fun findByProductId(@Param("productId") productId: Long): List } diff --git a/shop-backend/src/test/kotlin/no/nav/shopbackend/controller/ProductControllerTest.kt b/shop-backend/src/test/kotlin/no/nav/shopbackend/controller/ProductControllerTest.kt index d3d4a93..f44e7be 100644 --- a/shop-backend/src/test/kotlin/no/nav/shopbackend/controller/ProductControllerTest.kt +++ b/shop-backend/src/test/kotlin/no/nav/shopbackend/controller/ProductControllerTest.kt @@ -1,22 +1,23 @@ package no.nav.shopbackend.controller import com.fasterxml.jackson.databind.ObjectMapper -import java.util.Optional import no.nav.shopbackend.model.Product import no.nav.shopbackend.model.Rating import no.nav.shopbackend.repo.ProductRepository import no.nav.shopbackend.repo.RatingRepository import no.nav.shopbackend.service.SentimentService import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.ArgumentMatchers.anyLong -import org.mockito.Mockito.times +import org.mockito.ArgumentCaptor import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.PageRequest import org.springframework.http.MediaType import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.test.web.servlet.MockMvc @@ -29,13 +30,17 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status class ProductControllerTest { @Autowired private lateinit var mockMvc: MockMvc - @MockBean private lateinit var productRepo: ProductRepository - @MockBean private lateinit var ratingRepo: RatingRepository @MockBean private lateinit var sentimentService: SentimentService - @Test - fun testGetProductById() { - val product = + @MockBean private lateinit var productRepository: ProductRepository + + @MockBean private lateinit var ratingRepository: RatingRepository + + private lateinit var product: Product + + @BeforeEach + fun setUp() { + product = Product( "Test Product", "A test product", @@ -46,13 +51,38 @@ class ProductControllerTest { 1L ) - product.ratings.add(Rating(5, "Great product", "positive", product)) - product.ratings.add(Rating(1, "Bad product", "negative", product)) - product.ratings.add(Rating(3, "Average product", "neutral", product)) - product.ratings.add(Rating(4, "Good product", "neutral", product)) + val ratings = + mutableListOf( + Rating(5, "Great product", "positive", product, 1L), + Rating(1, "Bad product", "negative", product, 2L), + Rating(3, "Average product", "neutral", product, 3L), + Rating(4, "Good product", "neutral", product, 4L) + ) + + for (rating in ratings) { + product.ratings.add(rating) + } - `when`(productRepo.findById(anyLong())).thenReturn(Optional.ofNullable(product)) + `when`(productRepository.findAll(PageRequest.of(0, 10))).thenReturn(PageImpl(listOf(product))) + `when`(productRepository.findById(1L)).thenReturn(java.util.Optional.of(product)) + `when`(ratingRepository.findByProductId(1L)).thenReturn(ratings) + } + @Test + fun testGetProducts() { + val result = + mockMvc + .perform(get("/api/products").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk) + .andReturn() + + val expected = + """{"content":[{"name":"Test Product","description":"A test product","category":"TEE_SHIRT","price":13.37,"images":[],"id":1,"averageRating":3.25}],"pageable":"INSTANCE","totalPages":1,"totalElements":1,"last":true,"numberOfElements":1,"first":true,"size":1,"number":0,"sort":{"sorted":false,"unsorted":true,"empty":true},"empty":false}""" + assertEquals(expected, result.response.contentAsString) + } + + @Test + fun testGetProductById() { val result = mockMvc .perform(get("/api/products/1").accept(MediaType.APPLICATION_JSON)) @@ -65,31 +95,45 @@ class ProductControllerTest { } @Test - fun testPostReview() { - val product = - Product( - "Test Product", - "A test product", - Product.Category.TEE_SHIRT, - 13.37, - emptyList(), - mutableListOf(), - 1L - ) + fun testPostReviewForProduct() { + `when`(sentimentService.getSentiment("Super awesome product")) + .thenReturn(SentimentService.SentimentResponse("positive", 4.0f, 0.8f)) - `when`(productRepo.findById(anyLong())).thenReturn(Optional.ofNullable(product)) - `when`(sentimentService.getSentiment("Good product")) - .thenReturn(SentimentService.SentimentResponse("neutral", 0.0f, 0.0f)) - - val rating = Rating(4, "Good product", null, product) + val rating = Rating(5, "Super awesome product", null, product) val json = ObjectMapper().writeValueAsString(rating) - mockMvc - .perform( - post("/api/products/1/ratings").contentType(MediaType.APPLICATION_JSON).content(json) - ) - .andExpect(status().isOk) + val result = + mockMvc + .perform( + post("/api/products/1/ratings") + .contentType(MediaType.APPLICATION_JSON) + .content(json) + ) + .andExpect(status().isOk) + .andReturn() - verify(ratingRepo, times(1)).save(Rating(4, "Good product", "neutral", product)) + // inspect what ratingRepository.save was called with + // Capture the argument passed to ratingRepository.save + val captor = ArgumentCaptor.forClass(Rating::class.java) + verify(ratingRepository).save(captor.capture()) + + // Assert that the captured argument has the expected values + val savedRating = captor.value + assertEquals(5, savedRating.stars) + assertEquals("Super awesome product", savedRating.comment) + assertEquals("positive", savedRating.sentiment) + } + + @Test + fun testGetReviewsForProduct() { + val result = + mockMvc + .perform(get("/api/products/1/ratings").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk) + .andReturn() + + val expected = + """[{"stars":5,"comment":"Great product","sentiment":"positive","id":1},{"stars":1,"comment":"Bad product","sentiment":"negative","id":2},{"stars":3,"comment":"Average product","sentiment":"neutral","id":3},{"stars":4,"comment":"Good product","sentiment":"neutral","id":4}]""" + assertEquals(expected, result.response.contentAsString) } }