mirror of
https://github.com/Dannecron/spring-boot-demo.git
synced 2025-12-25 16:22:35 +03:00
move controllers to sub-project, fix dependencies
This commit is contained in:
@@ -50,11 +50,11 @@ allprojects {
|
||||
implementation(rootProject.libs.kotlin.reflect)
|
||||
implementation(rootProject.libs.kotlinx.serialization.json)
|
||||
implementation(rootProject.libs.logback.encoder)
|
||||
implementation(rootProject.libs.spring.aspects)
|
||||
implementation(rootProject.libs.springFramework.aspects)
|
||||
|
||||
testImplementation(rootProject.libs.kotlin.test.junit)
|
||||
testImplementation(rootProject.libs.mockito.kotlin)
|
||||
testImplementation(rootProject.libs.spring.boot.starter.test)
|
||||
testImplementation(rootProject.libs.springBoot.starter.test)
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
@@ -81,34 +81,23 @@ dependencies {
|
||||
implementation(project(":edge-contracts"))
|
||||
implementation(project(":db"))
|
||||
implementation(project(":edge-producing"))
|
||||
implementation(project(":edge-integration"))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":edge-consuming"))
|
||||
implementation(project(":edge-rest"))
|
||||
|
||||
implementation(libs.jackson.datatype.jsr)
|
||||
implementation(libs.jackson.module.kotlin)
|
||||
implementation(libs.ktor.client.cio)
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.postgres)
|
||||
implementation(libs.spring.boot.starter.jdbc)
|
||||
implementation(libs.spring.boot.starter.mustache)
|
||||
implementation(libs.spring.boot.starter.validation)
|
||||
implementation(libs.spring.boot.starter.web)
|
||||
implementation(libs.spring.cloud.starter.streamKafka)
|
||||
implementation(libs.spring.cloud.stream)
|
||||
implementation(libs.spring.doc.openapi.starter)
|
||||
implementation(libs.springBoot.starter.mustache)
|
||||
implementation(libs.springBoot.starter.web)
|
||||
|
||||
testImplementation(libs.ktor.client.mock)
|
||||
testImplementation(libs.spring.cloud.streamTestBinder)
|
||||
testImplementation(libs.testcontainers)
|
||||
testImplementation(libs.testcontainers.junit.jupiter)
|
||||
developmentOnly(libs.springBoot.devtools)
|
||||
|
||||
developmentOnly(libs.spring.boot.devtools)
|
||||
|
||||
kover(project(":edge-consuming"))
|
||||
kover(project(":core"))
|
||||
kover(project(":edge-producing"))
|
||||
kover(project(":db"))
|
||||
kover(project(":edge-contracts"))
|
||||
kover(project(":db"))
|
||||
kover(project(":edge-producing"))
|
||||
kover(project(":edge-integration"))
|
||||
kover(project(":core"))
|
||||
kover(project(":edge-consuming"))
|
||||
kover(project(":edge-rest"))
|
||||
}
|
||||
|
||||
tasks.bootJar {
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
package com.github.dannecron.demo.http.exceptions
|
||||
package com.github.dannecron.demo.edgerest
|
||||
|
||||
import com.github.dannecron.demo.http.responses.BadRequestResponse
|
||||
import com.github.dannecron.demo.http.responses.BaseResponse
|
||||
import com.github.dannecron.demo.http.responses.NotFoundResponse
|
||||
import com.github.dannecron.demo.http.responses.UnprocessableResponse
|
||||
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
|
||||
@@ -14,12 +18,16 @@ 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<Any> = ResponseEntity(
|
||||
fun handleMessageNotReadable(
|
||||
exception: HttpMessageNotReadableException,
|
||||
): ResponseEntity<BadRequestResponse> = ResponseEntity(
|
||||
BadRequestResponse(exception.message.toString()),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
)
|
||||
@@ -27,19 +35,21 @@ class ExceptionHandler {
|
||||
// 404
|
||||
@ExceptionHandler(NotFoundException::class)
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
fun handleNotFound(): ResponseEntity<Any> = ResponseEntity(NotFoundResponse(), 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<Any> = ResponseEntity(
|
||||
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<Any> = ResponseEntity(
|
||||
fun handleMethodArgumentNotValid(
|
||||
exception: MethodArgumentNotValidException,
|
||||
): ResponseEntity<UnprocessableResponse> = ResponseEntity(
|
||||
UnprocessableResponse(exception.javaClass.name, exception.allErrors),
|
||||
HttpStatus.UNPROCESSABLE_ENTITY,
|
||||
)
|
||||
@@ -47,8 +57,10 @@ class ExceptionHandler {
|
||||
// 500
|
||||
@ExceptionHandler(RuntimeException::class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
fun handleUnexpectedRuntimeException(exception: RuntimeException): ResponseEntity<Any> = ResponseEntity(
|
||||
BaseResponse(com.github.dannecron.demo.http.responses.ResponseStatus.INTERNAL_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)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
package com.github.dannecron.demo.http.controllers
|
||||
package com.github.dannecron.demo.edgerest.controllers
|
||||
|
||||
import com.github.dannecron.demo.BaseUnitTest
|
||||
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.http.responses.ResponseStatus
|
||||
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
|
||||
@@ -20,9 +23,19 @@ import java.util.UUID
|
||||
import kotlin.test.Test
|
||||
|
||||
@WebMvcTest(CustomerController::class)
|
||||
class CustomerControllerTest(
|
||||
@Autowired val mockMvc: MockMvc,
|
||||
): BaseUnitTest() {
|
||||
@AutoConfigureMockMvc
|
||||
@ContextConfiguration(
|
||||
classes = [
|
||||
WebTestConfig::class,
|
||||
CustomerController::class,
|
||||
ExceptionHandler::class,
|
||||
]
|
||||
)
|
||||
class CustomerControllerTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var mockMvc: MockMvc
|
||||
|
||||
@MockBean
|
||||
private lateinit var customerService: CustomerService
|
||||
|
||||
@@ -101,6 +114,6 @@ class CustomerControllerTest(
|
||||
mockMvc.get("/api/customer/$customerGuid")
|
||||
.andExpect { status { isNotFound() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { jsonPath("\$.status") { value(ResponseStatus.NOT_FOUND.status) } }
|
||||
.andExpect { jsonPath("\$.status") { value(ResponseStatusModel.NOT_FOUND.status) } }
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,31 @@
|
||||
package com.github.dannecron.demo.http.controllers
|
||||
package com.github.dannecron.demo.edgerest.controllers
|
||||
|
||||
import com.github.dannecron.demo.BaseUnitTest
|
||||
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(com.github.dannecron.demo.http.controllers.GreetingController::class)
|
||||
class GreetingControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() {
|
||||
@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")
|
||||
@@ -19,6 +35,7 @@ class GreetingControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
fun exampleHtml_shouldSeeRenderedHtml() {
|
||||
mockMvc.get("/example/html")
|
||||
.andExpect { status { isOk() } }
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package com.github.dannecron.demo.http.controllers
|
||||
package com.github.dannecron.demo.edgerest.controllers
|
||||
|
||||
import com.github.dannecron.demo.BaseUnitTest
|
||||
import com.github.dannecron.demo.core.dto.Product
|
||||
import com.github.dannecron.demo.core.services.product.ProductService
|
||||
import com.github.dannecron.demo.http.responses.ResponseStatus
|
||||
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
|
||||
@@ -14,12 +15,14 @@ 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
|
||||
@@ -30,7 +33,15 @@ import java.time.format.DateTimeFormatter
|
||||
import java.util.UUID
|
||||
|
||||
@WebMvcTest(ProductController::class)
|
||||
class ProductControllerTest: BaseUnitTest() {
|
||||
@AutoConfigureMockMvc
|
||||
@ContextConfiguration(
|
||||
classes = [
|
||||
WebTestConfig::class,
|
||||
ProductController::class,
|
||||
ExceptionHandler::class,
|
||||
]
|
||||
)
|
||||
class ProductControllerTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var mockMvc: MockMvc
|
||||
@@ -77,7 +88,7 @@ class ProductControllerTest: BaseUnitTest() {
|
||||
mockMvc.get("/api/product/$guid")
|
||||
.andExpect { status { isNotFound() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { jsonPath("\$.status") { value(ResponseStatus.NOT_FOUND.status) } }
|
||||
.andExpect { jsonPath("\$.status") { value(ResponseStatusModel.NOT_FOUND.status) } }
|
||||
|
||||
verify(productService, times(1)).findByGuid(guid)
|
||||
}
|
||||
@@ -132,7 +143,7 @@ class ProductControllerTest: BaseUnitTest() {
|
||||
}
|
||||
.andExpect { status { isBadRequest() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { jsonPath("\$.status") { value(ResponseStatus.BAD_REQUEST.status) } }
|
||||
.andExpect { jsonPath("\$.status") { value(ResponseStatusModel.BAD_REQUEST.status) } }
|
||||
.andExpect { jsonPath("\$.cause") { contains("name") } }
|
||||
|
||||
verifyNoInteractions(productService)
|
||||
@@ -148,7 +159,7 @@ class ProductControllerTest: BaseUnitTest() {
|
||||
}
|
||||
.andExpect { status { isUnprocessableEntity() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { jsonPath("\$.status") { value(ResponseStatus.UNPROCESSABLE.status) } }
|
||||
.andExpect { jsonPath("\$.status") { value(ResponseStatusModel.UNPROCESSABLE.status) } }
|
||||
.andExpect { jsonPath("\$.cause") { value(MethodArgumentNotValidException::class.qualifiedName) } }
|
||||
|
||||
verifyNoInteractions(productService)
|
||||
@@ -163,7 +174,7 @@ class ProductControllerTest: BaseUnitTest() {
|
||||
mockMvc.delete("/api/product/${guid}")
|
||||
.andExpect { status { isOk() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { jsonPath("\$.status") { value(ResponseStatus.OK.status) } }
|
||||
.andExpect { jsonPath("\$.status") { value(ResponseStatusModel.OK.status) } }
|
||||
|
||||
verify(productService, times(1)).delete(guid)
|
||||
}
|
||||
@@ -7,3 +7,5 @@ include("core")
|
||||
include("edge-consuming")
|
||||
include("edge-producing")
|
||||
include("edge-contracts")
|
||||
include("edge-rest")
|
||||
include("edge-integration")
|
||||
|
||||
@@ -3,16 +3,12 @@ package com.github.dannecron.demo.config
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import io.ktor.client.engine.HttpClientEngine
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.micrometer.observation.ObservationRegistry
|
||||
import io.micrometer.observation.aop.ObservedAspect
|
||||
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import com.github.dannecron.demo.services.neko.Client as NekoClient
|
||||
import com.github.dannecron.demo.services.neko.ClientImpl as NekoClientImpl
|
||||
|
||||
@Configuration
|
||||
class AppConfig {
|
||||
@@ -30,17 +26,5 @@ class AppConfig {
|
||||
|
||||
@Bean
|
||||
fun observedAspect(observationRegistry: ObservationRegistry) = ObservedAspect(observationRegistry)
|
||||
|
||||
@Bean
|
||||
fun httpClientEngine(): HttpClientEngine = CIO.create()
|
||||
|
||||
@Bean
|
||||
fun nekoClient(
|
||||
httpClientEngine: HttpClientEngine,
|
||||
@Value("\${neko.baseUrl}") baseUrl: String,
|
||||
): NekoClient = NekoClientImpl(
|
||||
engine = httpClientEngine,
|
||||
baseUrl = baseUrl,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.github.dannecron.demo.http.controllers
|
||||
|
||||
import com.github.dannecron.demo.core.services.customer.CustomerService
|
||||
import com.github.dannecron.demo.http.exceptions.NotFoundException
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import java.util.*
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = ["/api/customer"], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
class CustomerController(
|
||||
@Autowired
|
||||
private val customerService: CustomerService,
|
||||
) {
|
||||
@GetMapping("/{guid}")
|
||||
@Throws(NotFoundException::class)
|
||||
fun getCustomer(
|
||||
@PathVariable guid: UUID,
|
||||
): ResponseEntity<Any> {
|
||||
val customer = customerService.findByGuid(guid) ?: throw NotFoundException()
|
||||
|
||||
return ResponseEntity(customer, HttpStatus.OK)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.github.dannecron.demo.http.controllers
|
||||
|
||||
import com.github.dannecron.demo.providers.html.renderProductTable
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.ResponseBody
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
class GreetingController {
|
||||
@GetMapping("/greeting")
|
||||
fun greet(): String {
|
||||
return "Hello World!"
|
||||
}
|
||||
|
||||
@GetMapping(value = ["/example/html"], produces = ["text/html"])
|
||||
@ResponseBody
|
||||
fun exampleHtml(): String {
|
||||
return renderProductTable()
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.github.dannecron.demo.http.controllers
|
||||
|
||||
import com.github.dannecron.demo.http.responses.neko.ImagesResponse
|
||||
import com.github.dannecron.demo.services.neko.Client
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = ["/api/neko"], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
class NekoController(
|
||||
private val nekoClient: Client,
|
||||
) {
|
||||
@GetMapping("/categories")
|
||||
fun categories(): ResponseEntity<Any> = ResponseEntity(nekoClient.getCategories(), HttpStatus.OK)
|
||||
|
||||
@GetMapping("/images/{category}")
|
||||
fun images(
|
||||
@PathVariable category: String,
|
||||
@RequestParam imagesCount: Int = 1,
|
||||
): ResponseEntity<Any> = ResponseEntity(
|
||||
ImagesResponse(baseImages = nekoClient.getImages(category = category, amount = imagesCount)),
|
||||
HttpStatus.OK,
|
||||
)
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package com.github.dannecron.demo.http.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.http.exceptions.NotFoundException
|
||||
import com.github.dannecron.demo.http.exceptions.UnprocessableException
|
||||
import com.github.dannecron.demo.http.requests.CreateProductRequest
|
||||
import com.github.dannecron.demo.http.responses.NotFoundResponse
|
||||
import com.github.dannecron.demo.http.responses.makeOkResponse
|
||||
import com.github.dannecron.demo.http.responses.page.PageResponse
|
||||
import io.swagger.v3.oas.annotations.media.Content
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses
|
||||
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
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import java.util.*
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = ["/api/product"], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
class ProductController(
|
||||
private val productService: ProductService,
|
||||
) {
|
||||
@GetMapping("/{guid}")
|
||||
@Throws(NotFoundException::class)
|
||||
@ApiResponses(value = [
|
||||
ApiResponse(responseCode = "200", content = [
|
||||
Content(mediaType = "application/json", schema = Schema(implementation = Product::class)),
|
||||
]),
|
||||
ApiResponse(responseCode = "404", content = [
|
||||
Content(mediaType = "application/json", schema = Schema(implementation = NotFoundResponse::class))
|
||||
])
|
||||
])
|
||||
fun getProduct(
|
||||
@PathVariable guid: UUID,
|
||||
): ResponseEntity<Any> {
|
||||
val product = productService.findByGuid(guid = guid) ?: throw NotFoundException()
|
||||
|
||||
return ResponseEntity(product, HttpStatus.OK)
|
||||
}
|
||||
|
||||
@GetMapping("")
|
||||
@ApiResponses(value = [
|
||||
ApiResponse(responseCode = "200", content = [
|
||||
Content(mediaType = "application/json", schema = Schema(implementation = PageResponse::class)),
|
||||
]),
|
||||
])
|
||||
fun getProducts(
|
||||
@ParameterObject pageable: Pageable,
|
||||
): ResponseEntity<Any> {
|
||||
val products = productService.findAll(pageable)
|
||||
|
||||
return ResponseEntity(
|
||||
PageResponse(products),
|
||||
HttpStatus.OK,
|
||||
)
|
||||
}
|
||||
|
||||
@PostMapping("/{guid}/sync")
|
||||
@Throws(NotFoundException::class)
|
||||
fun syncProductToKafka(
|
||||
@PathVariable guid: UUID,
|
||||
@RequestParam(required = false) topic: String?
|
||||
): ResponseEntity<Any> {
|
||||
try {
|
||||
productService.send(guid, topic)
|
||||
} catch (_: InvalidDataException) {
|
||||
throw UnprocessableException("cannot sync product to kafka")
|
||||
}
|
||||
|
||||
return ResponseEntity(makeOkResponse(), HttpStatus.OK)
|
||||
}
|
||||
|
||||
@PostMapping(value = [""], consumes = [MediaType.APPLICATION_JSON_VALUE])
|
||||
fun createProduct(
|
||||
@Valid @RequestBody product: CreateProductRequest,
|
||||
): ResponseEntity<Any> {
|
||||
val saved = productService.create(
|
||||
product.name,
|
||||
product.price,
|
||||
product.description,
|
||||
)
|
||||
|
||||
return ResponseEntity(saved, HttpStatus.CREATED)
|
||||
}
|
||||
|
||||
@DeleteMapping("/{guid}")
|
||||
@Throws(NotFoundException::class, UnprocessableException::class)
|
||||
fun deleteProduct(
|
||||
@PathVariable guid: UUID,
|
||||
): ResponseEntity<Any> {
|
||||
try {
|
||||
productService.delete(guid)
|
||||
} catch (_: ProductNotFoundException) {
|
||||
throw NotFoundException()
|
||||
} catch (_: AlreadyDeletedException) {
|
||||
throw UnprocessableException("product already deleted")
|
||||
}
|
||||
|
||||
return ResponseEntity(makeOkResponse(), HttpStatus.OK)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package com.github.dannecron.demo.http.exceptions
|
||||
|
||||
class NotFoundException: RuntimeException()
|
||||
@@ -1,3 +0,0 @@
|
||||
package com.github.dannecron.demo.http.exceptions
|
||||
|
||||
class UnprocessableException(override val message: String): RuntimeException(message)
|
||||
@@ -1,5 +0,0 @@
|
||||
package com.github.dannecron.demo.http.responses
|
||||
|
||||
data class BadRequestResponse(
|
||||
val cause: String,
|
||||
): BaseResponse(status = ResponseStatus.BAD_REQUEST)
|
||||
@@ -1,5 +0,0 @@
|
||||
package com.github.dannecron.demo.http.responses
|
||||
|
||||
open class BaseResponse(val status: ResponseStatus)
|
||||
|
||||
fun makeOkResponse(): BaseResponse = BaseResponse(status = ResponseStatus.OK)
|
||||
@@ -1,3 +0,0 @@
|
||||
package com.github.dannecron.demo.http.responses
|
||||
|
||||
class NotFoundResponse: BaseResponse(ResponseStatus.NOT_FOUND)
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.github.dannecron.demo.http.responses
|
||||
|
||||
import org.springframework.validation.ObjectError
|
||||
|
||||
class UnprocessableResponse(
|
||||
val cause: String,
|
||||
val errors: List<ObjectError>?
|
||||
): BaseResponse(status = ResponseStatus.UNPROCESSABLE) {
|
||||
constructor(cause: String): this(cause, null)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.github.dannecron.demo.http.responses.neko
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import com.github.dannecron.demo.services.neko.dto.ImagesResponse as BaseResponse
|
||||
|
||||
@Serializable
|
||||
data class ImagesResponse(
|
||||
val images: List<Image>,
|
||||
) {
|
||||
constructor(baseImages: BaseResponse): this(
|
||||
images = baseImages.results.map {
|
||||
Image(it.url, it.animeName, it.artistHref, it.artistName, it.sourceUrl)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.github.dannecron.demo
|
||||
|
||||
import org.springframework.boot.test.context.TestConfiguration
|
||||
|
||||
open class BaseUnitTest {
|
||||
@TestConfiguration
|
||||
class TestConfig
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.github.dannecron.demo.http.controllers
|
||||
|
||||
import com.github.dannecron.demo.BaseUnitTest
|
||||
import com.github.dannecron.demo.services.neko.Client
|
||||
import com.github.dannecron.demo.services.neko.dto.Image
|
||||
import com.github.dannecron.demo.services.neko.dto.ImagesResponse
|
||||
import org.mockito.kotlin.doReturn
|
||||
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.http.MediaType
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.get
|
||||
import kotlin.test.Test
|
||||
|
||||
@WebMvcTest(NekoController::class)
|
||||
class NekoControllerTest(
|
||||
@Autowired val mockMvc: MockMvc,
|
||||
): BaseUnitTest() {
|
||||
@MockBean
|
||||
private lateinit var nekoClient: Client
|
||||
|
||||
@Test
|
||||
fun categories_success() {
|
||||
whenever(nekoClient.getCategories()) doReturn setOf("cat1", "cat2")
|
||||
|
||||
mockMvc.get("/api/neko/categories")
|
||||
.andExpect { status { isOk() } }
|
||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||
.andExpect { content { string("""["cat1","cat2"]""") } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun images_success() {
|
||||
val category = "some"
|
||||
val animeName = "boku no pico"
|
||||
whenever(nekoClient.getImages(category = category, amount = 1)) doReturn ImagesResponse(
|
||||
results = listOf(
|
||||
Image(
|
||||
"http://localhost",
|
||||
animeName = animeName,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
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) } }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user