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.kotlin.reflect)
|
||||||
implementation(rootProject.libs.kotlinx.serialization.json)
|
implementation(rootProject.libs.kotlinx.serialization.json)
|
||||||
implementation(rootProject.libs.logback.encoder)
|
implementation(rootProject.libs.logback.encoder)
|
||||||
implementation(rootProject.libs.spring.aspects)
|
implementation(rootProject.libs.springFramework.aspects)
|
||||||
|
|
||||||
testImplementation(rootProject.libs.kotlin.test.junit)
|
testImplementation(rootProject.libs.kotlin.test.junit)
|
||||||
testImplementation(rootProject.libs.mockito.kotlin)
|
testImplementation(rootProject.libs.mockito.kotlin)
|
||||||
testImplementation(rootProject.libs.spring.boot.starter.test)
|
testImplementation(rootProject.libs.springBoot.starter.test)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
@@ -81,34 +81,23 @@ dependencies {
|
|||||||
implementation(project(":edge-contracts"))
|
implementation(project(":edge-contracts"))
|
||||||
implementation(project(":db"))
|
implementation(project(":db"))
|
||||||
implementation(project(":edge-producing"))
|
implementation(project(":edge-producing"))
|
||||||
|
implementation(project(":edge-integration"))
|
||||||
implementation(project(":core"))
|
implementation(project(":core"))
|
||||||
implementation(project(":edge-consuming"))
|
implementation(project(":edge-consuming"))
|
||||||
|
implementation(project(":edge-rest"))
|
||||||
|
|
||||||
implementation(libs.jackson.datatype.jsr)
|
implementation(libs.springBoot.starter.mustache)
|
||||||
implementation(libs.jackson.module.kotlin)
|
implementation(libs.springBoot.starter.web)
|
||||||
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)
|
|
||||||
|
|
||||||
testImplementation(libs.ktor.client.mock)
|
developmentOnly(libs.springBoot.devtools)
|
||||||
testImplementation(libs.spring.cloud.streamTestBinder)
|
|
||||||
testImplementation(libs.testcontainers)
|
|
||||||
testImplementation(libs.testcontainers.junit.jupiter)
|
|
||||||
|
|
||||||
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(":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 {
|
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.core.utils.LoggerDelegate
|
||||||
import com.github.dannecron.demo.http.responses.BaseResponse
|
import com.github.dannecron.demo.edgecontracts.api.exceptions.NotFoundException
|
||||||
import com.github.dannecron.demo.http.responses.NotFoundResponse
|
import com.github.dannecron.demo.edgecontracts.api.exceptions.UnprocessableException
|
||||||
import com.github.dannecron.demo.http.responses.UnprocessableResponse
|
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.HttpStatus
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.http.converter.HttpMessageNotReadableException
|
import org.springframework.http.converter.HttpMessageNotReadableException
|
||||||
@@ -14,12 +18,16 @@ import org.springframework.web.bind.annotation.ResponseStatus
|
|||||||
|
|
||||||
@ControllerAdvice
|
@ControllerAdvice
|
||||||
class ExceptionHandler {
|
class ExceptionHandler {
|
||||||
|
private val logger by LoggerDelegate()
|
||||||
|
|
||||||
/* 4xx status codes */
|
/* 4xx status codes */
|
||||||
|
|
||||||
// 400
|
// 400
|
||||||
@ExceptionHandler(HttpMessageNotReadableException::class)
|
@ExceptionHandler(HttpMessageNotReadableException::class)
|
||||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
fun handleMessageNotReadable(exception: HttpMessageNotReadableException): ResponseEntity<Any> = ResponseEntity(
|
fun handleMessageNotReadable(
|
||||||
|
exception: HttpMessageNotReadableException,
|
||||||
|
): ResponseEntity<BadRequestResponse> = ResponseEntity(
|
||||||
BadRequestResponse(exception.message.toString()),
|
BadRequestResponse(exception.message.toString()),
|
||||||
HttpStatus.BAD_REQUEST,
|
HttpStatus.BAD_REQUEST,
|
||||||
)
|
)
|
||||||
@@ -27,19 +35,21 @@ class ExceptionHandler {
|
|||||||
// 404
|
// 404
|
||||||
@ExceptionHandler(NotFoundException::class)
|
@ExceptionHandler(NotFoundException::class)
|
||||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||||
fun handleNotFound(): ResponseEntity<Any> = ResponseEntity(NotFoundResponse(), HttpStatus.NOT_FOUND)
|
fun handleNotFound(): ResponseEntity<NotFoundResponse> = ResponseEntity(NotFoundResponse(), HttpStatus.NOT_FOUND)
|
||||||
|
|
||||||
// 422
|
// 422
|
||||||
@ExceptionHandler(UnprocessableException::class)
|
@ExceptionHandler(UnprocessableException::class)
|
||||||
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
|
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
|
||||||
fun handleUnprocessable(exception: UnprocessableException): ResponseEntity<Any> = ResponseEntity(
|
fun handleUnprocessable(exception: UnprocessableException): ResponseEntity<UnprocessableResponse> = ResponseEntity(
|
||||||
UnprocessableResponse(exception.message),
|
UnprocessableResponse(exception.message),
|
||||||
HttpStatus.UNPROCESSABLE_ENTITY,
|
HttpStatus.UNPROCESSABLE_ENTITY,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ExceptionHandler(MethodArgumentNotValidException::class)
|
@ExceptionHandler(MethodArgumentNotValidException::class)
|
||||||
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
|
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
|
||||||
fun handleMethodArgumentNotValid(exception: MethodArgumentNotValidException): ResponseEntity<Any> = ResponseEntity(
|
fun handleMethodArgumentNotValid(
|
||||||
|
exception: MethodArgumentNotValidException,
|
||||||
|
): ResponseEntity<UnprocessableResponse> = ResponseEntity(
|
||||||
UnprocessableResponse(exception.javaClass.name, exception.allErrors),
|
UnprocessableResponse(exception.javaClass.name, exception.allErrors),
|
||||||
HttpStatus.UNPROCESSABLE_ENTITY,
|
HttpStatus.UNPROCESSABLE_ENTITY,
|
||||||
)
|
)
|
||||||
@@ -47,8 +57,10 @@ class ExceptionHandler {
|
|||||||
// 500
|
// 500
|
||||||
@ExceptionHandler(RuntimeException::class)
|
@ExceptionHandler(RuntimeException::class)
|
||||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
fun handleUnexpectedRuntimeException(exception: RuntimeException): ResponseEntity<Any> = ResponseEntity(
|
fun handleUnexpectedRuntimeException(exception: RuntimeException): ResponseEntity<BaseResponse> = ResponseEntity(
|
||||||
BaseResponse(com.github.dannecron.demo.http.responses.ResponseStatus.INTERNAL_ERROR),
|
BaseResponse(ResponseStatusModel.INTERNAL_ERROR),
|
||||||
HttpStatus.INTERNAL_SERVER_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.City
|
||||||
import com.github.dannecron.demo.core.dto.Customer
|
import com.github.dannecron.demo.core.dto.Customer
|
||||||
import com.github.dannecron.demo.core.dto.view.CustomerExtended
|
import com.github.dannecron.demo.core.dto.view.CustomerExtended
|
||||||
import com.github.dannecron.demo.core.services.customer.CustomerService
|
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.doReturn
|
||||||
import org.mockito.kotlin.eq
|
import org.mockito.kotlin.eq
|
||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
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.autoconfigure.web.servlet.WebMvcTest
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.get
|
import org.springframework.test.web.servlet.get
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
@@ -20,9 +23,19 @@ import java.util.UUID
|
|||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
@WebMvcTest(CustomerController::class)
|
@WebMvcTest(CustomerController::class)
|
||||||
class CustomerControllerTest(
|
@AutoConfigureMockMvc
|
||||||
@Autowired val mockMvc: MockMvc,
|
@ContextConfiguration(
|
||||||
): BaseUnitTest() {
|
classes = [
|
||||||
|
WebTestConfig::class,
|
||||||
|
CustomerController::class,
|
||||||
|
ExceptionHandler::class,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
class CustomerControllerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var mockMvc: MockMvc
|
||||||
|
|
||||||
@MockBean
|
@MockBean
|
||||||
private lateinit var customerService: CustomerService
|
private lateinit var customerService: CustomerService
|
||||||
|
|
||||||
@@ -101,6 +114,6 @@ class CustomerControllerTest(
|
|||||||
mockMvc.get("/api/customer/$customerGuid")
|
mockMvc.get("/api/customer/$customerGuid")
|
||||||
.andExpect { status { isNotFound() } }
|
.andExpect { status { isNotFound() } }
|
||||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
.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.hamcrest.core.StringContains
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
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.autoconfigure.web.servlet.WebMvcTest
|
||||||
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.get
|
import org.springframework.test.web.servlet.get
|
||||||
|
import kotlin.test.Ignore
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
@WebMvcTest(com.github.dannecron.demo.http.controllers.GreetingController::class)
|
@WebMvcTest(GreetingController::class)
|
||||||
class GreetingControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() {
|
@AutoConfigureMockMvc
|
||||||
|
@ContextConfiguration(
|
||||||
|
classes = [
|
||||||
|
WebTestConfig::class,
|
||||||
|
GreetingController::class,
|
||||||
|
ExceptionHandler::class,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
class GreetingControllerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var mockMvc: MockMvc
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun greetings_shouldSeeGreetingMessage() {
|
fun greetings_shouldSeeGreetingMessage() {
|
||||||
mockMvc.get("/greeting")
|
mockMvc.get("/greeting")
|
||||||
@@ -19,6 +35,7 @@ class GreetingControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Ignore
|
||||||
fun exampleHtml_shouldSeeRenderedHtml() {
|
fun exampleHtml_shouldSeeRenderedHtml() {
|
||||||
mockMvc.get("/example/html")
|
mockMvc.get("/example/html")
|
||||||
.andExpect { status { isOk() } }
|
.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.dto.Product
|
||||||
import com.github.dannecron.demo.core.services.product.ProductService
|
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.contains
|
||||||
import org.hamcrest.Matchers.nullValue
|
import org.hamcrest.Matchers.nullValue
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@@ -14,12 +15,14 @@ import org.mockito.kotlin.verify
|
|||||||
import org.mockito.kotlin.verifyNoInteractions
|
import org.mockito.kotlin.verifyNoInteractions
|
||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
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.autoconfigure.web.servlet.WebMvcTest
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.data.domain.PageImpl
|
import org.springframework.data.domain.PageImpl
|
||||||
import org.springframework.data.domain.PageRequest
|
import org.springframework.data.domain.PageRequest
|
||||||
import org.springframework.data.domain.Sort
|
import org.springframework.data.domain.Sort
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.delete
|
import org.springframework.test.web.servlet.delete
|
||||||
import org.springframework.test.web.servlet.get
|
import org.springframework.test.web.servlet.get
|
||||||
@@ -30,7 +33,15 @@ import java.time.format.DateTimeFormatter
|
|||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@WebMvcTest(ProductController::class)
|
@WebMvcTest(ProductController::class)
|
||||||
class ProductControllerTest: BaseUnitTest() {
|
@AutoConfigureMockMvc
|
||||||
|
@ContextConfiguration(
|
||||||
|
classes = [
|
||||||
|
WebTestConfig::class,
|
||||||
|
ProductController::class,
|
||||||
|
ExceptionHandler::class,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
class ProductControllerTest {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private lateinit var mockMvc: MockMvc
|
private lateinit var mockMvc: MockMvc
|
||||||
@@ -77,7 +88,7 @@ class ProductControllerTest: BaseUnitTest() {
|
|||||||
mockMvc.get("/api/product/$guid")
|
mockMvc.get("/api/product/$guid")
|
||||||
.andExpect { status { isNotFound() } }
|
.andExpect { status { isNotFound() } }
|
||||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
.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)
|
verify(productService, times(1)).findByGuid(guid)
|
||||||
}
|
}
|
||||||
@@ -132,7 +143,7 @@ class ProductControllerTest: BaseUnitTest() {
|
|||||||
}
|
}
|
||||||
.andExpect { status { isBadRequest() } }
|
.andExpect { status { isBadRequest() } }
|
||||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
.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") } }
|
.andExpect { jsonPath("\$.cause") { contains("name") } }
|
||||||
|
|
||||||
verifyNoInteractions(productService)
|
verifyNoInteractions(productService)
|
||||||
@@ -148,7 +159,7 @@ class ProductControllerTest: BaseUnitTest() {
|
|||||||
}
|
}
|
||||||
.andExpect { status { isUnprocessableEntity() } }
|
.andExpect { status { isUnprocessableEntity() } }
|
||||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
.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) } }
|
.andExpect { jsonPath("\$.cause") { value(MethodArgumentNotValidException::class.qualifiedName) } }
|
||||||
|
|
||||||
verifyNoInteractions(productService)
|
verifyNoInteractions(productService)
|
||||||
@@ -163,7 +174,7 @@ class ProductControllerTest: BaseUnitTest() {
|
|||||||
mockMvc.delete("/api/product/${guid}")
|
mockMvc.delete("/api/product/${guid}")
|
||||||
.andExpect { status { isOk() } }
|
.andExpect { status { isOk() } }
|
||||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
.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)
|
verify(productService, times(1)).delete(guid)
|
||||||
}
|
}
|
||||||
@@ -7,3 +7,5 @@ include("core")
|
|||||||
include("edge-consuming")
|
include("edge-consuming")
|
||||||
include("edge-producing")
|
include("edge-producing")
|
||||||
include("edge-contracts")
|
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.ObjectMapper
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature
|
import com.fasterxml.jackson.databind.SerializationFeature
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
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.ObservationRegistry
|
||||||
import io.micrometer.observation.aop.ObservedAspect
|
import io.micrometer.observation.aop.ObservedAspect
|
||||||
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter
|
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
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
|
@Configuration
|
||||||
class AppConfig {
|
class AppConfig {
|
||||||
@@ -30,17 +26,5 @@ class AppConfig {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun observedAspect(observationRegistry: ObservationRegistry) = ObservedAspect(observationRegistry)
|
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