From 04d102c7e1d56fe816faf4958f630aa2fcb84428 Mon Sep 17 00:00:00 2001 From: Swordsteel Date: Sat, 13 Sep 2025 01:05:16 +0200 Subject: [PATCH] add TransactionController --- http/transaction.http | 8 ++ .../lulz/controller/TransactionController.kt | 40 +++++++ .../controller/TransactionControllerTest.kt | 105 ++++++++++++++++++ 3 files changed, 153 insertions(+) create mode 100644 http/transaction.http create mode 100644 src/main/kotlin/ltd/lulz/controller/TransactionController.kt create mode 100644 src/test/kotlin/ltd/lulz/controller/TransactionControllerTest.kt diff --git a/http/transaction.http b/http/transaction.http new file mode 100644 index 0000000..afb8972 --- /dev/null +++ b/http/transaction.http @@ -0,0 +1,8 @@ +### Deposit +POST {{url}}/deposit +Content-Type: application/json + +{ + "account": "00000000-0000-0000-0000-000000000000", + "amount": 1.23 +} diff --git a/src/main/kotlin/ltd/lulz/controller/TransactionController.kt b/src/main/kotlin/ltd/lulz/controller/TransactionController.kt new file mode 100644 index 0000000..4936234 --- /dev/null +++ b/src/main/kotlin/ltd/lulz/controller/TransactionController.kt @@ -0,0 +1,40 @@ +package ltd.lulz.controller + +import io.github.oshai.kotlinlogging.KotlinLogging +import jakarta.validation.Valid +import ltd.lulz.exception.AccountNotFoundException +import ltd.lulz.model.Transaction +import ltd.lulz.service.TransactionService +import org.springframework.http.HttpStatus.CREATED +import org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR +import org.springframework.http.HttpStatus.NOT_FOUND +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.server.ResponseStatusException +import reactor.core.publisher.Mono + +private val log = KotlinLogging.logger {} + +@Validated +@RestController +class TransactionController( + private val transactionService: TransactionService, +) { + + @PostMapping("/deposit") + @ResponseStatus(CREATED) + fun deposit( + @Valid @RequestBody request: Transaction.Request, + ): Mono = transactionService.deposit(request.account, request.amount) + .onErrorResume { + when (it) { + is AccountNotFoundException -> Mono.error(ResponseStatusException(NOT_FOUND)) + else -> Mono.error(ResponseStatusException(INTERNAL_SERVER_ERROR)) + } + } + .doOnError { log.warn { "deposit account ${request.account}: " + it.localizedMessage } } + .then() +} diff --git a/src/test/kotlin/ltd/lulz/controller/TransactionControllerTest.kt b/src/test/kotlin/ltd/lulz/controller/TransactionControllerTest.kt new file mode 100644 index 0000000..c1ea8b6 --- /dev/null +++ b/src/test/kotlin/ltd/lulz/controller/TransactionControllerTest.kt @@ -0,0 +1,105 @@ +package ltd.lulz.controller + +import io.mockk.every +import io.mockk.mockk +import java.math.BigDecimal +import java.util.UUID +import ltd.lulz.controller.AccountControllerTest.Companion.name +import ltd.lulz.exception.AccountNotFoundException +import ltd.lulz.model.AccountEntity +import ltd.lulz.model.Transaction +import ltd.lulz.service.TransactionService +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.http.MediaType.APPLICATION_JSON +import org.springframework.test.web.reactive.server.WebTestClient +import reactor.core.publisher.Mono + +@Suppress("ReactiveStreamsUnusedPublisher") +class TransactionControllerTest { + + companion object { + val amount: BigDecimal = BigDecimal.valueOf(0.01) + val uuid: UUID = UUID.fromString("00000000-0000-0000-0000-000000000000") + } + + private val transactionService: TransactionService = mockk() + private lateinit var webTestClient: WebTestClient + + @BeforeEach + fun setUp() { + webTestClient = WebTestClient.bindToController(TransactionController(transactionService)).build() + } + + @Test + fun `deposit success`() { + // given + val request = Transaction.Request(uuid, amount) + + every { transactionService.deposit(any(), any()) } returns Mono.just( + AccountEntity(id = uuid, name = name, amount = amount), + ) + + // when + val result = webTestClient.post() + .uri("/deposit") + .contentType(APPLICATION_JSON) + .bodyValue(request) + .exchange() + + // then + result.expectStatus().isCreated + } + + @Test + fun `deposit fail amount to small`() { + // given + val request = Transaction.Request(uuid, BigDecimal.valueOf(0.009)) + + // when + val result = webTestClient.post() + .uri("/deposit") + .contentType(APPLICATION_JSON) + .bodyValue(request) + .exchange() + + // then + result.expectStatus().isBadRequest + } + + @Test + fun `deposit default error`() { + // given + val request = Transaction.Request(uuid, amount) + + every { transactionService.deposit(any(), any()) } returns Mono.error(RuntimeException()) + + // when + val result = webTestClient.post() + .uri("/deposit") + .contentType(APPLICATION_JSON) + .bodyValue(request) + .exchange() + + // then + result.expectStatus().is5xxServerError + } + + @Test + fun `deposit account not found`() { + // given + val request = Transaction.Request(uuid, amount) + + every { transactionService.deposit(any(), any()) } returns Mono.error(AccountNotFoundException()) + + // when + val result = webTestClient.post() + .uri("/deposit") + .contentType(APPLICATION_JSON) + .bodyValue(request) + .exchange() + + // then + result.expectStatus().isNotFound + } +}