10 Commits

Author SHA1 Message Date
37dde70194 add TransactionService 2025-09-14 18:12:11 +02:00
e46bf55232 add account not found exception
- update balance in AccountController with onErrorResume
- update AccountService
  - update getForUpdateById with switchIfEmpty
  - update getById with switchIfEmpty
- add AccountNotFoundException
2025-09-14 18:12:11 +02:00
25f534b6e3 update AccountService with getForUpdateById and save 2025-09-14 18:12:11 +02:00
c96604ae79 update AccountController with balance 2025-09-14 18:12:11 +02:00
7ce1b15bca update AccountService with getById 2025-09-14 18:12:11 +02:00
85c140a815 add AccountController 2025-09-14 18:12:11 +02:00
dfadf203de add AccountUtil.kt
- AccountEntity toResponse
- Account Request toEntity
2025-09-14 18:12:11 +02:00
79357e4f4d add AccountService 2025-09-14 18:12:11 +02:00
12bc74c1e6 add account data
- AccountRepository
- AccountEntity
- 001-accounts.sql
- Account with Request and Response
2025-09-14 18:12:11 +02:00
83897285e4 project setup
- update README.md
- setup tls
  - update gradle.properties with docker tls ports
  - update application.yml
    - disable tls in develop
    - set docker values
    - add default tls values
  - add keystore.p12
- setup postgres
  - update application.yml with defualt values
  - add 000-initizalise.sql
  - add postgres dependencies and config
  - add docker compose development
- add actuator.http
- add http environment
- update name and readme for service
2025-09-14 18:12:11 +02:00
26 changed files with 824 additions and 10 deletions

View File

@@ -9,10 +9,10 @@ insert_final_newline = true
max_line_length = 120
tab_width = 4
[*.{json,md,txt,xml,yaml,yml}]
[*.{http,json,md,txt,xml,yaml,yml}]
max_line_length = 1024
[*.{json,xml,yaml,yml}]
[*.{http,json,xml,yaml,yml}]
indent_size = 2
tab_width = 2

View File

@@ -1,12 +1,20 @@
# {service}
# Basic Banking
{description}
This Monolith is pretend to be different services, it also Postgres 18rc1 to have access to UUIDv7.
## Properties For Deployment
| Name | Required | Information |
|------------------------|:--------:|-------------------------|
|-------------------------------|:--------:|-------------------------|
| spring.profiles.active | ✔ | Spring Boot environment |
| spring.r2dbc.url | ✔ | Postgres host url |
| spring.r2dbc.username | ✓ | Postgres username |
| spring.r2dbc.password | ✱ | Postgres password |
| server.port | | HTTP port |
| server.ssl.enabled | | HTTP Enable SSL |
| server.ssl.key-store | ✗ | HTTP Keystore |
| server.ssl.key-store-type | | HTTP Cert Type |
| server.ssl.key-store-password | ✱ | HTTP Cert Pass |
*Required:*
@@ -14,6 +22,10 @@
- *✗ mounted file.*
- *✱ need to be stored as secret.*
## Development
Use `development-compose.yml` to set up needed external dependencies.
## Releasing Service
Run release pipeline from `master` branch.

View File

@@ -12,14 +12,21 @@ dependencies {
implementation(aa.kotlin.reflect)
implementation(aa.kotlinx.coroutines)
implementation(aa.springboot.starter.actuator)
implementation(aa.springboot.starter.r2dbc)
implementation(aa.springboot.starter.validation)
implementation(aa.springboot.starter.webflux)
runtimeOnly(aa.postgresql)
runtimeOnly(aa.postgresql.r2dbc)
testImplementation(aa.kotlin.junit5)
testImplementation(aa.kotlinx.coroutines.test)
testImplementation(aa.mockk)
testImplementation(aa.reactor.test)
testImplementation(aa.springboot.starter.test)
testRuntimeOnly(aa.junit.platform.launcher)
}
group = "ltd.lulz"
description = "service template"
description = "service basic banking"

24
development-compose.yml Normal file
View File

@@ -0,0 +1,24 @@
name: develop
networks:
develop:
name: develop
external: true
volumes:
postgres:
services:
postgres:
image: postgres:18rc1-alpine
container_name: postgres
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- 5432:5432
networks:
- develop
volumes:
- postgres:/var/lib/postgresql/data

View File

@@ -1,2 +1,5 @@
version=0.1.0-SNAPSHOT
catalog=0.1.0
catalog=0.2.0-SNAPSHOT
docker.port.expose=8443
container.port.expose=8443
container.port.host=8443

11
http/account.http Normal file
View File

@@ -0,0 +1,11 @@
### Create Account
POST {{url}}/account
Content-Type: application/json
{
"name": "account name",
"amount": -1.11
}
### Create Account
GET {{url}}/balance/account-00000000-0000-0000-0000-000000000000

3
http/actuator.http Normal file
View File

@@ -0,0 +1,3 @@
### Actuator
GET {{url}}/actuator

11
http/http-client.env.json Normal file
View File

@@ -0,0 +1,11 @@
{
"develop": {
"url": "http://localhost:8080"
},
"docker": {
"url": "https://localhost:8443"
},
"kubernetes": {
"url": "https://10.0.0.0"
}
}

20
instructions.md Normal file
View File

@@ -0,0 +1,20 @@
# Senior Engineer Test
### **Develop a service that simulates basic banking operations in a programming language of your choice. This service will manage accounts, process deposits, withdrawals, and transfers between accounts.**
### The system should be designed reflecting real-world constraints of a bank.
## Requirements:
1. A class or set of functions that allow:
* Account creation: Allow users to create an account with an initial deposit.
* Deposit: Enable users to deposit money into their account.
* Withdrawal: Allow users to withdraw money from their account, ensuring that overdrafts are not allowed.
* Transfer: Enable transferring funds between accounts.
* Account balance: Provide the ability to check the account balance.
2. Database:
* In-memory data storage will suffice, no need to have a database alongside the project, but you can add one at your discretion
The word “service” here is used in a “software component/module” rather “deployable unit with an API” sense, no need to provide API for it.
## Feel free to take as along as you need to complete the exercise. This will be used as a base for a follow-up pair programming session.

View File

@@ -34,4 +34,4 @@ pluginManagement.repositories {
gradlePluginPortal()
}
rootProject.name = "service"
rootProject.name = "basic-banking"

View File

@@ -0,0 +1,64 @@
-- Role: role_administrator
-- DROP ROLE IF EXISTS role_administrator;
CREATE ROLE role_owner;
-- Role: role_service
-- DROP ROLE IF EXISTS role_service;
CREATE ROLE role_service;
-- Role: role_maintainer
-- DROP ROLE IF EXISTS role_maintainer;
CREATE ROLE role_maintainer;
-- Role: support_role
-- DROP ROLE IF EXISTS support_role;
CREATE ROLE role_support;
-- User: services
-- DROP USER IF EXISTS services;
CREATE USER service WITH PASSWORD 'password';
-- Assign role to the user
GRANT role_service TO service;
-- User: user_maintainer
-- DROP USER IF EXISTS user_maintainer;
CREATE USER user_maintainer WITH PASSWORD 'password';
-- Assign role to the user
GRANT role_maintainer TO user_maintainer;
-- User: user_support
-- DROP USER IF EXISTS user_support;
CREATE USER user_support WITH PASSWORD 'password';
-- Assign role to the user
GRANT role_support TO user_support;
-- Database: basic_banking
-- DROP DATABASE IF EXISTS basic_banking;
CREATE DATABASE basic_banking
WITH
OWNER = role_owner
ENCODING = 'UTF8'
TABLESPACE = pg_default
CONNECTION LIMIT = -1
IS_TEMPLATE = False;
COMMENT ON DATABASE basic_banking
IS 'Database for basic banking, registered account and transactions.';

View File

@@ -0,0 +1,21 @@
-- Table: public.accounts
DROP TABLE IF EXISTS public.accounts;
CREATE TABLE IF NOT EXISTS public.accounts
(
id UUID DEFAULT uuidv7(),
name VARCHAR(50) NOT NULL,
amount NUMERIC(19, 2) NOT NULL,
CONSTRAINT pk_contact_types PRIMARY KEY (id)
);
ALTER TABLE IF EXISTS public.accounts
OWNER to role_owner;
-- Revoke all permissions from existing roles
REVOKE ALL ON TABLE public.accounts FROM role_service, role_support;
-- Grant appropriate permissions
GRANT ALL ON TABLE public.accounts TO role_maintainer;
GRANT SELECT, INSERT, UPDATE ON TABLE public.accounts TO role_service;
GRANT SELECT ON TABLE public.accounts TO role_support;

View File

@@ -0,0 +1,44 @@
package ltd.lulz.controller
import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.validation.Valid
import java.util.UUID
import ltd.lulz.model.Account
import ltd.lulz.service.AccountService
import ltd.lulz.util.toEntity
import ltd.lulz.util.toResponse
import org.springframework.http.HttpStatus.CREATED
import org.springframework.http.HttpStatus.NOT_FOUND
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
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 {}
@RestController
@Validated
class AccountController(
private val accountService: AccountService,
) {
@PostMapping("/account")
@ResponseStatus(CREATED)
fun create(
@Valid @RequestBody request: Account.Request,
): Mono<Account.Response> = accountService.create(request.toEntity())
.map { it.toResponse() }
@GetMapping("/balance/account-{account}")
fun balance(
@PathVariable account: UUID,
): Mono<Account.Response> = accountService.getById(account)
.map { it.toResponse() }
.onErrorResume { Mono.error(ResponseStatusException(NOT_FOUND)) }
.doOnError { log.debug { "account $account not found for balance" } }
}

View File

@@ -0,0 +1,10 @@
package ltd.lulz.exception
@Suppress("unused")
class AccountNotFoundException : RuntimeException {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
}

View File

@@ -0,0 +1,22 @@
package ltd.lulz.model
import jakarta.validation.constraints.DecimalMin
import jakarta.validation.constraints.NotEmpty
import java.math.BigDecimal
import java.util.UUID
object Account {
data class Request(
@field:NotEmpty
val name: String,
@field:DecimalMin(value = "0.01")
val amount: BigDecimal,
)
data class Response(
val id: UUID,
val name: String,
val amount: BigDecimal,
)
}

View File

@@ -0,0 +1,14 @@
package ltd.lulz.model
import java.math.BigDecimal
import java.util.UUID
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Table
@Table("accounts")
data class AccountEntity(
@Id
val id: UUID? = null,
val name: String,
val amount: BigDecimal,
)

View File

@@ -0,0 +1,15 @@
package ltd.lulz.repository
import java.util.UUID
import ltd.lulz.model.AccountEntity
import org.springframework.data.r2dbc.repository.Query
import org.springframework.data.repository.reactive.ReactiveCrudRepository
import org.springframework.stereotype.Repository
import reactor.core.publisher.Mono
@Repository
interface AccountRepository : ReactiveCrudRepository<AccountEntity, UUID> {
@Query("SELECT * FROM accounts WHERE id = :id FOR UPDATE NOWAIT")
fun findByIdForUpdate(id: UUID): Mono<AccountEntity>
}

View File

@@ -0,0 +1,43 @@
package ltd.lulz.service
import io.github.oshai.kotlinlogging.KotlinLogging
import java.util.UUID
import ltd.lulz.exception.AccountNotFoundException
import ltd.lulz.model.AccountEntity
import ltd.lulz.repository.AccountRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import reactor.core.publisher.Mono
private val log = KotlinLogging.logger {}
@Service
class AccountService(
private val accountRepository: AccountRepository,
) {
fun create(
entity: AccountEntity,
): Mono<AccountEntity> = accountRepository
.save(entity)
.doOnNext { log.debug { "account created with id: ${it.id}" } }
fun getById(
id: UUID,
): Mono<AccountEntity> = accountRepository.findById(id)
.doOnNext { log.debug { "found account by id: ${it.id}" } }
.switchIfEmpty(Mono.error(AccountNotFoundException()))
@Transactional
fun getForUpdateById(
id: UUID,
): Mono<AccountEntity> = accountRepository.findByIdForUpdate(id)
.doOnNext { log.trace { "account with id: ${it.id} locked for update" } }
.switchIfEmpty(Mono.error(AccountNotFoundException()))
@Transactional
fun save(
entity: AccountEntity,
): Mono<AccountEntity> = accountRepository.save(entity)
.doOnNext { log.trace { "account with id: ${it.id} saved" } }
}

View File

@@ -0,0 +1,26 @@
package ltd.lulz.service
import io.github.oshai.kotlinlogging.KotlinLogging
import java.math.BigDecimal
import java.util.UUID
import ltd.lulz.model.AccountEntity
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import reactor.core.publisher.Mono
private val log = KotlinLogging.logger {}
@Service
class TransactionService(
private val accountService: AccountService,
) {
@Transactional
fun deposit(
id: UUID,
amount: BigDecimal,
): Mono<AccountEntity> = accountService.getForUpdateById(id)
.map { it.copy(amount = it.amount + amount) }
.doOnNext { log.trace { "Deposited $amount to account ${it.id}" } }
.flatMap { accountService.save(it) }
}

View File

@@ -0,0 +1,18 @@
package ltd.lulz.util
import java.math.RoundingMode.DOWN
import ltd.lulz.model.Account
import ltd.lulz.model.AccountEntity
import org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR
import org.springframework.web.server.ResponseStatusException
fun Account.Request.toEntity(): AccountEntity = AccountEntity(
name = name,
amount = amount.setScale(2, DOWN),
)
fun AccountEntity.toResponse(): Account.Response = Account.Response(
id = id ?: throw ResponseStatusException(INTERNAL_SERVER_ERROR),
name = name,
amount = amount,
)

View File

@@ -24,6 +24,13 @@ management:
exposure:
include: "health,info"
server:
port: 8443
ssl:
enabled: true
key-store: classpath:cert/keystore.p12
key-store-type: PKCS12
---
###########################
### Develop environment ###
@@ -32,6 +39,16 @@ spring:
config:
activate:
on-profile: develop
r2dbc:
url: r2dbc:postgresql://localhost:5432/basic_banking
username: service
password: password
server:
port: 8080
ssl:
enabled: false
# key-store-password: password
---
##########################
@@ -41,6 +58,14 @@ spring:
config:
activate:
on-profile: docker
r2dbc:
url: r2dbc:postgresql://postgres:5432/basic_banking
username: service
password: password
server:
ssl:
key-store-password: password
---
##############################
@@ -50,3 +75,5 @@ spring:
config:
activate:
on-profile: kubernetes
r2dbc:
url: r2dbc:postgresql://postgres:5432/basic_banking

Binary file not shown.

View File

@@ -0,0 +1,132 @@
package ltd.lulz.controller
import io.mockk.every
import io.mockk.mockk
import java.math.BigDecimal
import java.util.UUID
import ltd.lulz.exception.AccountNotFoundException
import ltd.lulz.model.Account
import ltd.lulz.model.AccountEntity
import ltd.lulz.service.AccountService
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
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("MayBeConstant", "ReactiveStreamsUnusedPublisher")
class AccountControllerTest {
companion object {
val name: String = "some name"
val amount: BigDecimal = BigDecimal.valueOf(0.01)
val uuid: UUID = UUID.fromString("00000000-0000-0000-0000-000000000000")
}
private val accountService: AccountService = mockk()
private lateinit var webTestClient: WebTestClient
@BeforeEach
fun setUp() {
webTestClient = WebTestClient.bindToController(AccountController(accountService)).build()
}
@Nested
inner class CreateAccount {
@Test
fun `create account success`() {
// given
val request = Account.Request(name, amount)
every { accountService.create(any()) } returns Mono.just(
AccountEntity(id = uuid, name = name, amount = amount),
)
// when
val result = webTestClient.post()
.uri("/account")
.contentType(APPLICATION_JSON)
.bodyValue(request)
.exchange()
// then
result.expectStatus().isCreated
.expectBody()
.jsonPath("$.id").isEqualTo(uuid.toString())
.jsonPath("$.name").isEqualTo(name)
.jsonPath("$.amount").isEqualTo(amount.toString())
}
@Test
fun `create account fail no name`() {
// given
val request = Account.Request("", amount)
// when
val result = webTestClient.post()
.uri("/account")
.contentType(APPLICATION_JSON)
.bodyValue(request)
.exchange()
// then
result.expectStatus().isBadRequest
}
@Test
fun `create account fail zero or less amount`() {
// given
val request = Account.Request("name", BigDecimal.valueOf(0))
// when
val result = webTestClient.post()
.uri("/account")
.contentType(APPLICATION_JSON)
.bodyValue(request)
.exchange()
// then
result.expectStatus().isBadRequest
}
}
@Nested
inner class AccountBalance {
@Test
fun `account balance success`() {
// given
every { accountService.getById(any()) } returns Mono.just(
AccountEntity(id = uuid, name = name, amount = amount),
)
// when
val result = webTestClient.get()
.uri("/balance/account-$uuid")
.exchange()
// then
result.expectStatus().isOk
.expectBody()
.jsonPath("$.id").isEqualTo(uuid.toString())
.jsonPath("$.name").isEqualTo(name)
.jsonPath("$.amount").isEqualTo(amount.toString())
}
@Test
fun `account balance fail`() {
// given
every { accountService.getById(any()) } returns Mono.error(AccountNotFoundException())
// when
val result = webTestClient.get()
.uri("/balance/account-$uuid")
.exchange()
// then
result.expectStatus().isNotFound
}
}
}

View File

@@ -0,0 +1,138 @@
package ltd.lulz.service
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import java.math.BigDecimal
import java.util.UUID
import ltd.lulz.exception.AccountNotFoundException
import ltd.lulz.model.AccountEntity
import ltd.lulz.repository.AccountRepository
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import reactor.core.publisher.Mono
import reactor.test.StepVerifier
@Suppress("ReactiveStreamsUnusedPublisher", "MayBeConstant")
class AccountServiceTest {
companion object {
val name: String = "some name"
val amount: BigDecimal = BigDecimal.valueOf(0.01)
val uuid: UUID = UUID.fromString("00000000-0000-0000-0000-000000000000")
}
private var repository: AccountRepository = mockk()
private lateinit var service: AccountService
@BeforeEach
fun setUp() {
service = AccountService(repository)
}
@Test
fun `create account`() {
// given
val entity = AccountEntity(name = name, amount = amount)
val capture = slot<AccountEntity>()
every { repository.save(capture(capture)) } answers { Mono.just(capture.captured.copy(id = uuid)) }
// when stepped
StepVerifier.create(service.create(entity))
.assertNext { result ->
assertThat(result.id).isEqualTo(uuid)
assertThat(result.name).isEqualTo(name)
assertThat(result.amount).isEqualTo(amount)
}
.verifyComplete()
verify { repository.save(any()) }
}
@Test
fun `get by id`() {
// given
val capture = slot<UUID>()
every { repository.findById(capture(capture)) }
.answers { Mono.just(AccountEntity(capture.captured, name, amount)) }
// when stepped
StepVerifier.create(service.getById(uuid))
.assertNext { result ->
assertThat(result.id).isEqualTo(uuid)
assertThat(result.name).isEqualTo(name)
assertThat(result.amount).isEqualTo(amount)
}
.verifyComplete()
verify { repository.findById(any(UUID::class)) }
}
@Test
fun `get by id - fail`() {
// given
every { repository.findById(any(UUID::class)) } returns Mono.empty()
// when stepped
StepVerifier.create(service.getById(uuid))
.expectError(AccountNotFoundException::class.java)
.verify()
verify { repository.findById(any(UUID::class)) }
}
@Test
fun `get for update by id`() {
// given
val capture = slot<UUID>()
every { repository.findByIdForUpdate(capture(capture)) }
.answers { Mono.just(AccountEntity(capture.captured, name, amount)) }
// when stepped
StepVerifier.create(service.getForUpdateById(uuid))
.assertNext { result ->
assertThat(result.id).isEqualTo(uuid)
assertThat(result.name).isEqualTo(name)
assertThat(result.amount).isEqualTo(amount)
}
.verifyComplete()
verify { repository.findByIdForUpdate(any(UUID::class)) }
}
@Test
fun `get for update by id - fail`() {
// given
every { repository.findByIdForUpdate(any(UUID::class)) } returns Mono.empty()
// when stepped
StepVerifier.create(service.getForUpdateById(uuid))
.expectError(AccountNotFoundException::class.java)
.verify()
verify { repository.findByIdForUpdate(any(UUID::class)) }
}
@Test
fun `save change`() {
// given
val entity = AccountEntity(name = name, amount = amount)
val capture = slot<AccountEntity>()
every { repository.save(capture(capture)) }
.answers { Mono.just(capture.captured) }
// when stepped
StepVerifier.create(service.save(entity))
.assertNext { result ->
assertThat(result).isEqualTo(entity)
}
.verifyComplete()
verify { repository.save(any()) }
}
}

View File

@@ -0,0 +1,74 @@
package ltd.lulz.service
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import java.math.BigDecimal
import java.util.UUID
import ltd.lulz.exception.AccountNotFoundException
import ltd.lulz.model.AccountEntity
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import reactor.core.publisher.Mono
import reactor.test.StepVerifier
@Suppress("MayBeConstant", "ReactiveStreamsUnusedPublisher")
class TransactionServiceTest {
companion object {
val name: String = "some name"
val amount: BigDecimal = BigDecimal.valueOf(1.01)
val uuid: UUID = UUID.fromString("00000000-0000-0000-0000-000000000000")
}
private val accountService: AccountService = mockk()
private lateinit var service: TransactionService
@BeforeEach
fun setup() {
service = TransactionService(accountService)
}
@Test
fun `deposit to account - success`() {
// given
val deposit = BigDecimal.valueOf(1.10)
val capture = slot<UUID>()
every { accountService.getForUpdateById(capture(capture)) }
.answers { Mono.just(AccountEntity(capture.captured, name, amount)) }
val entity = slot<AccountEntity>()
every { accountService.save(capture(entity)) }
.answers { Mono.just(entity.captured) }
// when stepped
StepVerifier.create(service.deposit(uuid, deposit))
.assertNext { result ->
assertThat(result.id).isEqualTo(uuid)
assertThat(result.name).isEqualTo(name)
assertThat(result.amount).isEqualTo(amount + deposit)
}
.verifyComplete()
verify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) }
verify(exactly = 1) { accountService.save(any()) }
}
@Test
fun `deposit to account - account not found`() {
// given
val deposit = BigDecimal.valueOf(1.10)
every { accountService.getForUpdateById(any()) } returns Mono.error(AccountNotFoundException())
// when stepped
StepVerifier.create(service.deposit(uuid, deposit))
.expectError(AccountNotFoundException::class.java)
.verify()
verify(exactly = 1) { accountService.getForUpdateById(any(UUID::class)) }
verify(exactly = 0) { accountService.save(any()) }
}
}

View File

@@ -0,0 +1,75 @@
package ltd.lulz.util
import java.math.BigDecimal
import java.util.UUID
import kotlin.test.Test
import ltd.lulz.model.Account
import ltd.lulz.model.AccountEntity
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.assertThrows
import org.springframework.web.server.ResponseStatusException
@Suppress("MayBeConstant")
class AccountUtilKtTest {
companion object {
val name: String = "some name"
val amount: BigDecimal = BigDecimal.valueOf(0.01)
val uuid: UUID = UUID.fromString("00000000-0000-0000-0000-000000000000")
}
@Test
fun `account request to account entity`() {
// given
val request = Account.Request(name, amount)
// when
val result = request.toEntity()
// then
assertThat(result.id).isNull()
assertThat(result.name).isEqualTo(name)
assertThat(result.amount).isEqualTo(amount)
}
@Test
fun `account request to account entity cut decimals`() {
// given
val request = Account.Request(name, BigDecimal.valueOf(0.0099))
// when
val result = request.toEntity()
// then
assertThat(result.id).isNull()
assertThat(result.name).isEqualTo(name)
assertThat(result.amount.toString()).isEqualTo("0.00")
}
@Test
fun `account entity to account response`() {
// given
val entity = AccountEntity(uuid, name, amount)
// when
val result = entity.toResponse()
// then
assertThat(result.id).isEqualTo(uuid)
assertThat(result.name).isEqualTo(name)
}
@Test
fun `account entity to account response - fail`() {
// given
val entity = AccountEntity(null, name, amount)
// when
val exception = assertThrows<ResponseStatusException> {
entity.toResponse()
}
// then
assertThat(exception.message).isEqualTo("500 INTERNAL_SERVER_ERROR")
}
}