mirror of
https://github.com/Dannecron/spring-boot-demo.git
synced 2025-12-26 00:32:34 +03:00
move controllers to sub-project, fix dependencies
This commit is contained in:
7
edge-rest/build.gradle.kts
Normal file
7
edge-rest/build.gradle.kts
Normal file
@@ -0,0 +1,7 @@
|
||||
dependencies {
|
||||
implementation(project(":edge-contracts"))
|
||||
implementation(project(":core"))
|
||||
|
||||
implementation(rootProject.libs.springBoot.starter.web)
|
||||
implementation(rootProject.libs.springData.commons)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.github.dannecron.demo.edgerest
|
||||
|
||||
import com.github.dannecron.demo.core.utils.LoggerDelegate
|
||||
import com.github.dannecron.demo.edgecontracts.api.exceptions.NotFoundException
|
||||
import com.github.dannecron.demo.edgecontracts.api.exceptions.UnprocessableException
|
||||
import com.github.dannecron.demo.edgecontracts.api.model.ResponseStatusModel
|
||||
import com.github.dannecron.demo.edgecontracts.api.response.common.BadRequestResponse
|
||||
import com.github.dannecron.demo.edgecontracts.api.response.common.BaseResponse
|
||||
import com.github.dannecron.demo.edgecontracts.api.response.common.NotFoundResponse
|
||||
import com.github.dannecron.demo.edgecontracts.api.response.common.UnprocessableResponse
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.bind.annotation.ResponseStatus
|
||||
|
||||
@ControllerAdvice
|
||||
class ExceptionHandler {
|
||||
private val logger by LoggerDelegate()
|
||||
|
||||
/* 4xx status codes */
|
||||
|
||||
// 400
|
||||
@ExceptionHandler(HttpMessageNotReadableException::class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
fun handleMessageNotReadable(
|
||||
exception: HttpMessageNotReadableException,
|
||||
): ResponseEntity<BadRequestResponse> = ResponseEntity(
|
||||
BadRequestResponse(exception.message.toString()),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
)
|
||||
|
||||
// 404
|
||||
@ExceptionHandler(NotFoundException::class)
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
fun handleNotFound(): ResponseEntity<NotFoundResponse> = ResponseEntity(NotFoundResponse(), HttpStatus.NOT_FOUND)
|
||||
|
||||
// 422
|
||||
@ExceptionHandler(UnprocessableException::class)
|
||||
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
|
||||
fun handleUnprocessable(exception: UnprocessableException): ResponseEntity<UnprocessableResponse> = ResponseEntity(
|
||||
UnprocessableResponse(exception.message),
|
||||
HttpStatus.UNPROCESSABLE_ENTITY,
|
||||
)
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException::class)
|
||||
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
|
||||
fun handleMethodArgumentNotValid(
|
||||
exception: MethodArgumentNotValidException,
|
||||
): ResponseEntity<UnprocessableResponse> = ResponseEntity(
|
||||
UnprocessableResponse(exception.javaClass.name, exception.allErrors),
|
||||
HttpStatus.UNPROCESSABLE_ENTITY,
|
||||
)
|
||||
|
||||
// 500
|
||||
@ExceptionHandler(RuntimeException::class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
fun handleUnexpectedRuntimeException(exception: RuntimeException): ResponseEntity<BaseResponse> = ResponseEntity(
|
||||
BaseResponse(ResponseStatusModel.INTERNAL_ERROR),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
).also {
|
||||
logger.error("internal server error", exception)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.github.dannecron.demo.edgerest.controllers
|
||||
|
||||
import com.github.dannecron.demo.core.dto.City
|
||||
import com.github.dannecron.demo.core.dto.Customer
|
||||
import com.github.dannecron.demo.core.dto.view.CustomerExtended
|
||||
import com.github.dannecron.demo.core.services.customer.CustomerService
|
||||
import com.github.dannecron.demo.edgecontracts.api.CustomerApi
|
||||
import com.github.dannecron.demo.edgecontracts.api.exceptions.NotFoundException
|
||||
import com.github.dannecron.demo.edgecontracts.api.model.CityApiModel
|
||||
import com.github.dannecron.demo.edgecontracts.api.model.CustomerApiModel
|
||||
import com.github.dannecron.demo.edgecontracts.api.response.GetCustomerResponse
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import java.util.UUID
|
||||
|
||||
@RestController
|
||||
class CustomerController(
|
||||
private val customerService: CustomerService,
|
||||
) : CustomerApi {
|
||||
|
||||
@Throws(NotFoundException::class)
|
||||
override fun getCustomer(guid: UUID): ResponseEntity<GetCustomerResponse> {
|
||||
val customerExtended = customerService.findByGuid(guid) ?: throw NotFoundException(null)
|
||||
|
||||
return ResponseEntity(customerExtended.toResponse(), HttpStatus.OK)
|
||||
}
|
||||
|
||||
private fun CustomerExtended.toResponse() = GetCustomerResponse(
|
||||
customer = customer.toApiModel(),
|
||||
city = city?.toApiModel()
|
||||
)
|
||||
|
||||
private fun Customer.toApiModel() = CustomerApiModel(
|
||||
id = id,
|
||||
guid = guid,
|
||||
name = name,
|
||||
cityId = cityId,
|
||||
createdAt = createdAt,
|
||||
updatedAt = updatedAt,
|
||||
)
|
||||
|
||||
private fun City.toApiModel() = CityApiModel(
|
||||
id = id,
|
||||
guid = guid,
|
||||
name = name,
|
||||
createdAt = createdAt,
|
||||
updatedAt = updatedAt,
|
||||
deletedAt = deletedAt,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.github.dannecron.demo.edgerest.controllers
|
||||
|
||||
import com.github.dannecron.demo.edgecontracts.api.GreetingApi
|
||||
import org.springframework.web.bind.annotation.ResponseBody
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
class GreetingController() : GreetingApi {
|
||||
override fun greet(): String {
|
||||
return "Hello World!"
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
override fun exampleHtml(): String {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.github.dannecron.demo.edgerest.controllers
|
||||
|
||||
import com.github.dannecron.demo.core.dto.neko.ImageDto
|
||||
import com.github.dannecron.demo.core.services.neko.NekoService
|
||||
import com.github.dannecron.demo.edgecontracts.api.NekoApi
|
||||
import com.github.dannecron.demo.edgecontracts.api.model.NekoImageApiModel
|
||||
import com.github.dannecron.demo.edgecontracts.api.response.GetNekoImagesResponse
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
class NekoController(
|
||||
private val nekoService: NekoService
|
||||
) : NekoApi {
|
||||
|
||||
override fun categories(): ResponseEntity<Set<String>> =
|
||||
ResponseEntity(nekoService.getCategories(), HttpStatus.OK)
|
||||
|
||||
override fun images(category: String, imagesCount: Int): ResponseEntity<GetNekoImagesResponse> =
|
||||
ResponseEntity(
|
||||
GetNekoImagesResponse(
|
||||
nekoService.getImages(category, imagesCount).map { it.toApiModel() }
|
||||
),
|
||||
HttpStatus.OK,
|
||||
)
|
||||
|
||||
private fun ImageDto.toApiModel() = NekoImageApiModel(
|
||||
url = url,
|
||||
animeName = animeName,
|
||||
artistHref = artistHref,
|
||||
artistName = artistName,
|
||||
sourceUrl = sourceUrl,
|
||||
isGif = animeName == null
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.github.dannecron.demo.edgerest.controllers
|
||||
|
||||
import com.github.dannecron.demo.core.dto.Product
|
||||
import com.github.dannecron.demo.core.exceptions.AlreadyDeletedException
|
||||
import com.github.dannecron.demo.core.exceptions.InvalidDataException
|
||||
import com.github.dannecron.demo.core.exceptions.ProductNotFoundException
|
||||
import com.github.dannecron.demo.core.services.product.ProductService
|
||||
import com.github.dannecron.demo.edgecontracts.api.ProductApi
|
||||
import com.github.dannecron.demo.edgecontracts.api.exceptions.NotFoundException
|
||||
import com.github.dannecron.demo.edgecontracts.api.exceptions.UnprocessableException
|
||||
import com.github.dannecron.demo.edgecontracts.api.model.ProductApiModel
|
||||
import com.github.dannecron.demo.edgecontracts.api.request.CreateProductRequest
|
||||
import com.github.dannecron.demo.edgecontracts.api.response.common.BaseResponse
|
||||
import com.github.dannecron.demo.edgecontracts.api.response.common.makeOkResponse
|
||||
import com.github.dannecron.demo.edgecontracts.api.response.page.PageResponse
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import java.util.UUID
|
||||
|
||||
@RestController
|
||||
class ProductController(
|
||||
private val productService: ProductService,
|
||||
) : ProductApi {
|
||||
override fun getProducts(pageable: Pageable): ResponseEntity<PageResponse<ProductApiModel>> {
|
||||
val products = productService.findAll(pageable)
|
||||
|
||||
return ResponseEntity(
|
||||
PageResponse(products.map { it.toApiModel() }),
|
||||
HttpStatus.OK,
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(NotFoundException::class)
|
||||
override fun getProduct(guid: UUID): ResponseEntity<ProductApiModel> {
|
||||
val product = productService.findByGuid(guid = guid) ?: throw NotFoundException(null)
|
||||
|
||||
return ResponseEntity(product.toApiModel(), HttpStatus.OK)
|
||||
}
|
||||
|
||||
override fun createProduct(product: CreateProductRequest): ResponseEntity<ProductApiModel> {
|
||||
val saved = productService.create(
|
||||
product.name,
|
||||
product.price,
|
||||
product.description,
|
||||
)
|
||||
|
||||
return ResponseEntity(saved.toApiModel(), HttpStatus.CREATED)
|
||||
}
|
||||
|
||||
@Throws(NotFoundException::class, UnprocessableException::class)
|
||||
override fun deleteProduct(guid: UUID): ResponseEntity<BaseResponse> {
|
||||
try {
|
||||
productService.delete(guid)
|
||||
return ResponseEntity(makeOkResponse(), HttpStatus.OK)
|
||||
} catch (ex: ProductNotFoundException) {
|
||||
throw NotFoundException(ex)
|
||||
} catch (ex: AlreadyDeletedException) {
|
||||
throw UnprocessableException("product already deleted", ex)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(NotFoundException::class)
|
||||
override fun sendProduct(guid: UUID, topic: String?): ResponseEntity<BaseResponse> {
|
||||
try {
|
||||
productService.send(guid, topic)
|
||||
|
||||
return ResponseEntity(makeOkResponse(), HttpStatus.OK)
|
||||
} catch (_: InvalidDataException) {
|
||||
throw UnprocessableException("cannot sync product")
|
||||
}
|
||||
}
|
||||
|
||||
private fun Product.toApiModel() = ProductApiModel(
|
||||
id = id,
|
||||
guid = guid,
|
||||
name = name,
|
||||
description = description,
|
||||
price = price,
|
||||
createdAt = createdAt,
|
||||
updatedAt = updatedAt,
|
||||
deletedAt = deletedAt,
|
||||
priceDouble = getPriceDouble(),
|
||||
isDeleted = isDeleted(),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.github.dannecron.demo.edgerest
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import org.springframework.boot.test.context.TestConfiguration
|
||||
import org.springframework.context.annotation.Bean
|
||||
|
||||
@TestConfiguration
|
||||
class WebTestConfig {
|
||||
|
||||
@Bean
|
||||
fun objectMapper(): ObjectMapper = ObjectMapper().apply {
|
||||
registerModules(JavaTimeModule())
|
||||
configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.github.dannecron.demo.edgerest.controllers
|
||||
|
||||
import com.github.dannecron.demo.core.dto.City
|
||||
import com.github.dannecron.demo.core.dto.Customer
|
||||
import com.github.dannecron.demo.core.dto.view.CustomerExtended
|
||||
import com.github.dannecron.demo.core.services.customer.CustomerService
|
||||
import com.github.dannecron.demo.edgecontracts.api.model.ResponseStatusModel
|
||||
import com.github.dannecron.demo.edgerest.ExceptionHandler
|
||||
import com.github.dannecron.demo.edgerest.WebTestConfig
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.whenever
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||
import org.springframework.boot.test.mock.mockito.MockBean
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.get
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
import kotlin.test.Test
|
||||
|
||||
@WebMvcTest(CustomerController::class)
|
||||
@AutoConfigureMockMvc
|
||||
@ContextConfiguration(
|
||||
classes = [
|
||||
WebTestConfig::class,
|
||||
CustomerController::class,
|
||||
ExceptionHandler::class,
|
||||
]
|
||||
)
|
||||
class CustomerControllerTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var mockMvc: MockMvc
|
||||
|
||||
@MockBean
|
||||
private lateinit var customerService: CustomerService
|
||||
|
||||
@Test
|
||||
fun getCustomer_successWithCity() {
|
||||
val customerId = 22L
|
||||
val cityId = 11L
|
||||
val customerGuid = UUID.randomUUID()
|
||||
val customerExtended = CustomerExtended(
|
||||
customer = Customer(
|
||||
id = customerId,
|
||||
guid = customerGuid,
|
||||
name = "Test Person",
|
||||
cityId = cityId,
|
||||
createdAt = OffsetDateTime.now().minusHours(1),
|
||||
updatedAt = OffsetDateTime.now(),
|
||||
),
|
||||
city = City(
|
||||
id = cityId,
|
||||
guid = UUID.randomUUID(),
|
||||
name = "Test City",
|
||||
createdAt = OffsetDateTime.now().minusWeeks(1),
|
||||
updatedAt = null,
|
||||
deletedAt = null
|
||||
)
|
||||
)
|
||||
|
||||
whenever(customerService.findByGuid(
|
||||
eq(customerGuid),
|
||||
)) doReturn customerExtended
|
||||
|
||||
mockMvc.get("/api/customer/$customerGuid")
|
||||
.andExpect { status { isOk() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { jsonPath("\$.customer.id") { value(customerId) } }
|
||||
.andExpect { jsonPath("\$.customer.cityId") { value(cityId) } }
|
||||
.andExpect { jsonPath("\$.city.id") { value(cityId) } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getCustomer_successNoCity() {
|
||||
val customerId = 22L
|
||||
val customerGuid = UUID.randomUUID()
|
||||
val customerExtended = CustomerExtended(
|
||||
customer = Customer(
|
||||
id = customerId,
|
||||
guid = customerGuid,
|
||||
name = "Test Person",
|
||||
cityId = null,
|
||||
createdAt = OffsetDateTime.now().minusHours(1),
|
||||
updatedAt = OffsetDateTime.now(),
|
||||
),
|
||||
city = null,
|
||||
)
|
||||
|
||||
whenever(customerService.findByGuid(
|
||||
eq(customerGuid),
|
||||
)) doReturn customerExtended
|
||||
|
||||
mockMvc.get("/api/customer/$customerGuid")
|
||||
.andExpect { status { isOk() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { jsonPath("\$.customer.id") { value(customerId) } }
|
||||
.andExpect { jsonPath("\$.customer.cityId") { value(null) } }
|
||||
.andExpect { jsonPath("\$.city") { value(null) } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getCustomer_successNotFound() {
|
||||
val customerGuid = UUID.randomUUID()
|
||||
|
||||
whenever(customerService.findByGuid(
|
||||
eq(customerGuid),
|
||||
)) doReturn null
|
||||
|
||||
mockMvc.get("/api/customer/$customerGuid")
|
||||
.andExpect { status { isNotFound() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { jsonPath("\$.status") { value(ResponseStatusModel.NOT_FOUND.status) } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.github.dannecron.demo.edgerest.controllers
|
||||
|
||||
import com.github.dannecron.demo.edgerest.ExceptionHandler
|
||||
import com.github.dannecron.demo.edgerest.WebTestConfig
|
||||
import org.hamcrest.core.StringContains
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.get
|
||||
import kotlin.test.Ignore
|
||||
import kotlin.test.Test
|
||||
|
||||
@WebMvcTest(GreetingController::class)
|
||||
@AutoConfigureMockMvc
|
||||
@ContextConfiguration(
|
||||
classes = [
|
||||
WebTestConfig::class,
|
||||
GreetingController::class,
|
||||
ExceptionHandler::class,
|
||||
]
|
||||
)
|
||||
class GreetingControllerTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var mockMvc: MockMvc
|
||||
|
||||
@Test
|
||||
fun greetings_shouldSeeGreetingMessage() {
|
||||
mockMvc.get("/greeting")
|
||||
.andExpect { status { isOk() } }
|
||||
.andExpect { content { contentType("text/plain;charset=UTF-8") } }
|
||||
.andExpect { content { string("Hello World!") } }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
fun exampleHtml_shouldSeeRenderedHtml() {
|
||||
mockMvc.get("/example/html")
|
||||
.andExpect { status { isOk() } }
|
||||
.andExpect { content { contentType("text/html;charset=UTF-8") } }
|
||||
.andExpect { content { string(StringContains("Product")) } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.github.dannecron.demo.edgerest.controllers
|
||||
|
||||
import com.github.dannecron.demo.core.dto.neko.ImageDto
|
||||
import com.github.dannecron.demo.core.exceptions.neko.IntegrationException
|
||||
import com.github.dannecron.demo.core.services.neko.NekoService
|
||||
import com.github.dannecron.demo.edgerest.ExceptionHandler
|
||||
import com.github.dannecron.demo.edgerest.WebTestConfig
|
||||
import org.mockito.kotlin.times
|
||||
import org.mockito.kotlin.verify
|
||||
import org.mockito.kotlin.whenever
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||
import org.springframework.boot.test.mock.mockito.MockBean
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.get
|
||||
import kotlin.test.Test
|
||||
|
||||
@WebMvcTest(NekoController::class)
|
||||
@AutoConfigureMockMvc
|
||||
@ContextConfiguration(
|
||||
classes = [
|
||||
WebTestConfig::class,
|
||||
NekoController::class,
|
||||
ExceptionHandler::class,
|
||||
]
|
||||
)
|
||||
class NekoControllerTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var mockMvc: MockMvc
|
||||
|
||||
@MockBean
|
||||
private lateinit var nekoService: NekoService
|
||||
|
||||
@Test
|
||||
fun `getCategories - success`() {
|
||||
whenever(nekoService.getCategories()).thenReturn(setOf("cat1", "cat2"))
|
||||
|
||||
mockMvc.get("/api/neko/categories")
|
||||
.andExpect { status { isOk() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { content { string("""["cat1","cat2"]""") } }
|
||||
|
||||
verify(nekoService, times(1)).getCategories()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCategories - fail - integration error`() {
|
||||
whenever(nekoService.getCategories()).thenThrow(IntegrationException("some error"))
|
||||
|
||||
mockMvc.get("/api/neko/categories")
|
||||
.andExpect { status { isInternalServerError() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { jsonPath("$.status") { value("internal") } }
|
||||
|
||||
verify(nekoService, times(1)).getCategories()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getImages - success`() {
|
||||
val category = "some"
|
||||
val animeName = "boku no pico"
|
||||
whenever(nekoService.getImages(category = category, amount = 1))
|
||||
.thenReturn(
|
||||
listOf(
|
||||
ImageDto(
|
||||
"http://localhost",
|
||||
animeName = animeName,
|
||||
artistHref = null,
|
||||
artistName = null,
|
||||
sourceUrl = null,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
mockMvc.get("/api/neko/images/$category")
|
||||
.andExpect { status { isOk() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { jsonPath("\$.images[0].animeName") { value(animeName) } }
|
||||
.andExpect { jsonPath("\$.images[0].artistName") { value(null) } }
|
||||
|
||||
verify(nekoService, times(1)).getImages(category = category, amount = 1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.github.dannecron.demo.edgerest.controllers
|
||||
|
||||
import com.github.dannecron.demo.core.dto.Product
|
||||
import com.github.dannecron.demo.core.services.product.ProductService
|
||||
import com.github.dannecron.demo.edgecontracts.api.model.ResponseStatusModel
|
||||
import com.github.dannecron.demo.edgerest.ExceptionHandler
|
||||
import com.github.dannecron.demo.edgerest.WebTestConfig
|
||||
import org.hamcrest.Matchers.contains
|
||||
import org.hamcrest.Matchers.nullValue
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.anyOrNull
|
||||
import org.mockito.kotlin.times
|
||||
import org.mockito.kotlin.verify
|
||||
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.AutoConfigureMockMvc
|
||||
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.context.ContextConfiguration
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.delete
|
||||
import org.springframework.test.web.servlet.get
|
||||
import org.springframework.test.web.servlet.post
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.UUID
|
||||
|
||||
@WebMvcTest(ProductController::class)
|
||||
@AutoConfigureMockMvc
|
||||
@ContextConfiguration(
|
||||
classes = [
|
||||
WebTestConfig::class,
|
||||
ProductController::class,
|
||||
ExceptionHandler::class,
|
||||
]
|
||||
)
|
||||
class ProductControllerTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var mockMvc: MockMvc
|
||||
|
||||
@MockBean
|
||||
private lateinit var productService: ProductService
|
||||
|
||||
private val guid = UUID.randomUUID()
|
||||
private val now = OffsetDateTime.now()
|
||||
private val productId = 12L
|
||||
private val productName = "new-product"
|
||||
private val productPrice = 20123L
|
||||
private val product = Product(
|
||||
id = productId,
|
||||
guid = guid,
|
||||
name = productName,
|
||||
description = null,
|
||||
price = productPrice,
|
||||
createdAt = now,
|
||||
updatedAt = null,
|
||||
deletedAt = null,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `getProduct - 200`() {
|
||||
whenever(productService.findByGuid(any())).thenReturn(product)
|
||||
|
||||
mockMvc.get("/api/product/$guid")
|
||||
.andExpect { status { isOk() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { jsonPath("\$.id") { value(product.id.toString()) } }
|
||||
.andExpect { jsonPath("\$.guid") { value(guid.toString()) } }
|
||||
.andExpect { jsonPath("\$.name") { value(productName) } }
|
||||
.andExpect { jsonPath("\$.createdAt") { value(now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) } }
|
||||
.andExpect { jsonPath("\$.updatedAt") { value(nullValue()) } }
|
||||
|
||||
verify(productService, times(1)).findByGuid(guid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getProduct - 404`() {
|
||||
whenever(productService.findByGuid(any())).thenReturn(null)
|
||||
|
||||
mockMvc.get("/api/product/$guid")
|
||||
.andExpect { status { isNotFound() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { jsonPath("\$.status") { value(ResponseStatusModel.NOT_FOUND.status) } }
|
||||
|
||||
verify(productService, times(1)).findByGuid(guid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getProducts - 200`() {
|
||||
val pageRequest = PageRequest.of(1, 2, Sort.by(Sort.Direction.DESC, "createdAt"))
|
||||
|
||||
whenever(productService.findAll(any()))
|
||||
.thenReturn(PageImpl(listOf(product)))
|
||||
|
||||
mockMvc.get("/api/product?page=1&size=2&sort=createdAt,desc")
|
||||
.andExpect { 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(productId) } }
|
||||
.andExpect { jsonPath("\$.data[0].name") { value(productName) } }
|
||||
.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(201.23) } }
|
||||
.andExpect { jsonPath("\$.data[0].isDeleted") { value(false) } }
|
||||
|
||||
verify(productService, times(1)).findAll(pageRequest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createProduct - 200`() {
|
||||
val reqBody = """{"name":"$productName","description":null,"price":$productPrice}"""
|
||||
|
||||
whenever(productService.create(any(), any(), anyOrNull())).thenReturn(product)
|
||||
|
||||
mockMvc.post("/api/product") {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
content = reqBody
|
||||
}
|
||||
.andExpect { status { isCreated() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { jsonPath("\$.id") { value(productId) } }
|
||||
|
||||
verify(productService, times(1)).create(productName, productPrice, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createProduct - 400 - no name param`() {
|
||||
val reqBody = """{"description":null,"price":$productPrice}"""
|
||||
|
||||
mockMvc.post("/api/product") {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
content = reqBody
|
||||
}
|
||||
.andExpect { status { isBadRequest() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { jsonPath("\$.status") { value(ResponseStatusModel.BAD_REQUEST.status) } }
|
||||
.andExpect { jsonPath("\$.cause") { contains("name") } }
|
||||
|
||||
verifyNoInteractions(productService)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createProduct - 400 - empty name param`() {
|
||||
val reqBody = """{"name":"","description":null,"price":$productPrice}"""
|
||||
|
||||
mockMvc.post("/api/product") {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
content = reqBody
|
||||
}
|
||||
.andExpect { status { isUnprocessableEntity() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { jsonPath("\$.status") { value(ResponseStatusModel.UNPROCESSABLE.status) } }
|
||||
.andExpect { jsonPath("\$.cause") { value(MethodArgumentNotValidException::class.qualifiedName) } }
|
||||
|
||||
verifyNoInteractions(productService)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deleteProduct - 200`() {
|
||||
val deletedProduct = product.copy(deletedAt = now)
|
||||
|
||||
whenever(productService.delete(any())).thenReturn(deletedProduct)
|
||||
|
||||
mockMvc.delete("/api/product/${guid}")
|
||||
.andExpect { status { isOk() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { jsonPath("\$.status") { value(ResponseStatusModel.OK.status) } }
|
||||
|
||||
verify(productService, times(1)).delete(guid)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user