diff --git a/src/main/kotlin/com/example/demo/config/AppConfig.kt b/src/main/kotlin/com/example/demo/config/AppConfig.kt index 6fae17f..1a546f1 100644 --- a/src/main/kotlin/com/example/demo/config/AppConfig.kt +++ b/src/main/kotlin/com/example/demo/config/AppConfig.kt @@ -10,18 +10,25 @@ import com.example.demo.services.database.product.ProductService import com.example.demo.services.database.product.ProductServiceImpl import com.example.demo.services.kafka.Producer import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration - @Configuration class AppConfig( @Value("\${kafka.producer.product.default-sync-topic}") private val defaultProductSyncTopic: String ) { @Bean - fun objectMapper(): ObjectMapper = ObjectMapper() + fun objectMapper(): ObjectMapper { + val objectMapper = ObjectMapper() + objectMapper.registerModules(JavaTimeModule()) + objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + + return objectMapper + } @Bean fun shopProvider(): ShopProvider = MockedShopProvider() @@ -39,3 +46,4 @@ class AppConfig( @Bean fun cityService(@Autowired cityRepository: CityRepository): CityService = CityServiceImpl(cityRepository) } + diff --git a/src/main/kotlin/com/example/demo/http/controllers/ProductController.kt b/src/main/kotlin/com/example/demo/http/controllers/ProductController.kt index f6becf8..4796aa2 100644 --- a/src/main/kotlin/com/example/demo/http/controllers/ProductController.kt +++ b/src/main/kotlin/com/example/demo/http/controllers/ProductController.kt @@ -9,6 +9,8 @@ import com.example.demo.services.database.product.ProductService import com.example.demo.services.database.product.exceptions.ProductNotFoundException import com.example.demo.services.kafka.exceptions.InvalidArgumentException import jakarta.validation.Valid +import org.springdoc.core.annotations.ParameterObject +import org.springframework.data.domain.Pageable import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity @@ -21,7 +23,6 @@ class ProductController( val productService: ProductService, ) { @GetMapping("/{guid}") - @ResponseBody @Throws(NotFoundException::class) fun getProduct( @PathVariable guid: UUID, @@ -31,8 +32,25 @@ class ProductController( return ResponseEntity(product, HttpStatus.OK) } + @GetMapping("") + fun getProducts( + @ParameterObject pageable: Pageable, + ): ResponseEntity { + val products = productService.findAll(pageable) + + return ResponseEntity( + mapOf( + "data" to products.content, + "meta" to mapOf( + "total" to products.totalElements, + "pages" to products.totalPages, + ), + ), + HttpStatus.OK, + ) + } + @PostMapping("/{guid}/sync") - @ResponseBody @Throws(NotFoundException::class) fun syncProductToKafka( @PathVariable guid: UUID, @@ -48,7 +66,6 @@ class ProductController( } @PostMapping(value = [""], consumes = [MediaType.APPLICATION_JSON_VALUE]) - @ResponseBody fun createProduct( @Valid @RequestBody product: CreateProductRequest, ): ResponseEntity { @@ -62,7 +79,6 @@ class ProductController( } @DeleteMapping("/{guid}") - @ResponseBody @Throws(NotFoundException::class, UnprocessableException::class) fun deleteProduct( @PathVariable guid: UUID, diff --git a/src/main/kotlin/com/example/demo/providers/ProductRepository.kt b/src/main/kotlin/com/example/demo/providers/ProductRepository.kt index 263200c..83e4798 100644 --- a/src/main/kotlin/com/example/demo/providers/ProductRepository.kt +++ b/src/main/kotlin/com/example/demo/providers/ProductRepository.kt @@ -3,13 +3,14 @@ package com.example.demo.providers import com.example.demo.models.Product import org.springframework.data.jdbc.repository.query.Query import org.springframework.data.repository.CrudRepository +import org.springframework.data.repository.PagingAndSortingRepository import org.springframework.data.repository.query.Param import org.springframework.stereotype.Repository import java.time.OffsetDateTime import java.util.* @Repository -interface ProductRepository: CrudRepository { +interface ProductRepository: CrudRepository, PagingAndSortingRepository { fun findByGuid(guid: UUID): Product? @Query(value = "UPDATE Product SET deleted_at = :deletedAt WHERE guid = :guid RETURNING *") diff --git a/src/main/kotlin/com/example/demo/services/database/product/ProductService.kt b/src/main/kotlin/com/example/demo/services/database/product/ProductService.kt index ae73a8b..cfade78 100644 --- a/src/main/kotlin/com/example/demo/services/database/product/ProductService.kt +++ b/src/main/kotlin/com/example/demo/services/database/product/ProductService.kt @@ -4,6 +4,8 @@ import com.example.demo.models.Product import com.example.demo.services.database.exceptions.AlreadyDeletedException import com.example.demo.services.database.product.exceptions.ProductNotFoundException import com.example.demo.services.kafka.exceptions.InvalidArgumentException +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service import java.util.* @@ -11,6 +13,8 @@ import java.util.* interface ProductService { fun findByGuid(guid: UUID): Product? + fun findAll(pageable: Pageable): Page + fun create(name: String, price: Long, description: String?): Product @Throws(ProductNotFoundException::class, AlreadyDeletedException::class) diff --git a/src/main/kotlin/com/example/demo/services/database/product/ProductServiceImpl.kt b/src/main/kotlin/com/example/demo/services/database/product/ProductServiceImpl.kt index 08f2ad3..39a08df 100644 --- a/src/main/kotlin/com/example/demo/services/database/product/ProductServiceImpl.kt +++ b/src/main/kotlin/com/example/demo/services/database/product/ProductServiceImpl.kt @@ -5,6 +5,8 @@ import com.example.demo.providers.ProductRepository import com.example.demo.services.database.exceptions.AlreadyDeletedException import com.example.demo.services.database.product.exceptions.ProductNotFoundException import com.example.demo.services.kafka.Producer +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import java.time.OffsetDateTime import java.util.* @@ -15,6 +17,8 @@ class ProductServiceImpl( ): ProductService { override fun findByGuid(guid: UUID): Product? = productRepository.findByGuid(guid) + override fun findAll(pageable: Pageable): Page = productRepository.findAll(pageable) + override fun create(name: String, price: Long, description: String?): Product { val product = Product( id = null, diff --git a/src/test/kotlin/com/example/demo/http/controllers/ProductControllerTest.kt b/src/test/kotlin/com/example/demo/http/controllers/ProductControllerTest.kt index 61d6d28..f195975 100644 --- a/src/test/kotlin/com/example/demo/http/controllers/ProductControllerTest.kt +++ b/src/test/kotlin/com/example/demo/http/controllers/ProductControllerTest.kt @@ -8,12 +8,16 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.hamcrest.Matchers.contains import org.hamcrest.Matchers.nullValue import org.junit.jupiter.api.Test +import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever 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.data.domain.Sort import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.delete @@ -76,6 +80,36 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() { .andExpect { jsonPath("\$.status") { value(ResponseStatus.NOT_FOUND.status) } } } + @Test + fun getProducts_success() { + val now = OffsetDateTime.now() + whenever(productService.findAll( + PageRequest.of(1, 2, Sort.by(Sort.Direction.DESC, "createdAt")), + )) doReturn PageImpl(listOf(Product( + id = 12, + guid = UUID.randomUUID(), + name = "some", + description = null, + price = 11130, + createdAt = now, + updatedAt = null, + deletedAt = null, + ))) + + mockMvc.get("/api/product?page=1&size=2&sort=created_at,desc") + .andExpect { status { status { isOk() } } } + .andExpect { content { contentType(MediaType.APPLICATION_JSON) } } + .andExpect { jsonPath("\$.meta.total") { value(1) } } + .andExpect { jsonPath("\$.meta.pages") { value(1) } } + .andExpect { jsonPath("\$.data") { isArray() } } + .andExpect { jsonPath("\$.data[0].id") { value(12) } } + .andExpect { jsonPath("\$.data[0].name") { value("some") } } + .andExpect { jsonPath("\$.data[0].description") { value(null) } } + .andExpect { jsonPath("\$.data[0].createdAt") { value(now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) } } + .andExpect { jsonPath("\$.data[0].priceDouble") { value(111.30) } } + .andExpect { jsonPath("\$.data[0].isDeleted") { value(false) } } + } + @Test fun createProduct_success() { val productId = 13.toLong()