diff --git a/accountant/accountant-core/pom.xml b/accountant/accountant-core/pom.xml
index 65ff8aef1..022876966 100644
--- a/accountant/accountant-core/pom.xml
+++ b/accountant/accountant-core/pom.xml
@@ -49,5 +49,9 @@
spring-tx
provided
+
+ org.mockito
+ mockito-core
+
diff --git a/api/api-ports/api-persister-postgres/pom.xml b/api/api-ports/api-persister-postgres/pom.xml
index 28ded2b84..310a95e50 100644
--- a/api/api-ports/api-persister-postgres/pom.xml
+++ b/api/api-ports/api-persister-postgres/pom.xml
@@ -1,67 +1,71 @@
-
-
- 4.0.0
-
-
- co.nilin.opex.api
- api
- 1.0-SNAPSHOT
- ../../pom.xml
-
-
- co.nilin.opex.api.ports.postgres
- api-persister-postgres
- api-persister-postgres
- Persist items of Opex api on Postgres
-
-
-
- org.jetbrains.kotlin
- kotlin-reflect
-
-
- co.nilin.opex.api.core
- api-core
-
-
- co.nilin.opex.utility.error
- error-handler
-
-
- org.springframework.boot
- spring-boot-starter-data-r2dbc
-
-
- io.r2dbc
- r2dbc-postgresql
- runtime
-
-
- org.postgresql
- postgresql
- runtime
-
-
- io.projectreactor.kotlin
- reactor-kotlin-extensions
-
-
- org.jetbrains.kotlinx
- kotlinx-coroutines-reactor
-
-
- org.jetbrains.kotlinx
- kotlinx-coroutines-core
-
-
- com.google.code.gson
- gson
-
-
- io.projectreactor
- reactor-test
- test
-
-
-
+
+
+ 4.0.0
+
+
+ co.nilin.opex.api
+ api
+ 1.0-SNAPSHOT
+ ../../pom.xml
+
+
+ co.nilin.opex.api.ports.postgres
+ api-persister-postgres
+ api-persister-postgres
+ Persist items of Opex api on Postgres
+
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+
+
+ co.nilin.opex.api.core
+ api-core
+
+
+ co.nilin.opex.utility.error
+ error-handler
+
+
+ org.springframework.boot
+ spring-boot-starter-data-r2dbc
+
+
+ io.r2dbc
+ r2dbc-postgresql
+ runtime
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+ io.projectreactor.kotlin
+ reactor-kotlin-extensions
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-reactor
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+
+
+ com.google.code.gson
+ gson
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+ org.mockito.kotlin
+ mockito-kotlin
+
+
+
diff --git a/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/MarketQueryHandlerTest.kt b/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/MarketQueryHandlerTest.kt
new file mode 100644
index 000000000..2241f4ab5
--- /dev/null
+++ b/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/MarketQueryHandlerTest.kt
@@ -0,0 +1,151 @@
+package co.nilin.opex.api.ports.postgres.impl
+
+import co.nilin.opex.api.core.inout.OrderDirection
+import co.nilin.opex.api.core.inout.OrderStatus
+import co.nilin.opex.api.core.spi.SymbolMapper
+import co.nilin.opex.api.ports.postgres.dao.OrderRepository
+import co.nilin.opex.api.ports.postgres.dao.OrderStatusRepository
+import co.nilin.opex.api.ports.postgres.dao.TradeRepository
+import co.nilin.opex.api.ports.postgres.impl.sample.Valid
+import kotlinx.coroutines.flow.count
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.runBlocking
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.mockito.kotlin.*
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
+
+class MarketQueryHandlerTest {
+ private val orderRepository: OrderRepository = mock()
+ private val tradeRepository: TradeRepository = mock()
+ private val orderStatusRepository: OrderStatusRepository = mock()
+ private val symbolMapper: SymbolMapper = mock()
+ private val marketQueryHandler =
+ MarketQueryHandlerImpl(orderRepository, tradeRepository, orderStatusRepository, symbolMapper)
+
+ @Test
+ fun givenAggregatedOrderPrice_whenOpenASKOrders_thenReturnOrderBookResponseList(): Unit = runBlocking {
+ stubbing(orderRepository) {
+ on {
+ findBySymbolAndDirectionAndStatusSortAscendingByPrice(
+ eq(Valid.ETH_USDT),
+ eq(OrderDirection.ASK),
+ eq(1),
+ argThat {
+ this == listOf(
+ OrderStatus.NEW.code,
+ OrderStatus.PARTIALLY_FILLED.code
+ )
+ }
+ )
+ } doReturn Flux.just(Valid.AGGREGATED_ORDER_PRICE_MODEL)
+ }
+
+ val orderBookResponses = marketQueryHandler.openAskOrders(Valid.ETH_USDT, 1)
+
+ assertThat(orderBookResponses).isNotNull
+ assertThat(orderBookResponses.size).isEqualTo(1)
+ assertThat(orderBookResponses.first()).isEqualTo(Valid.ORDER_BOOK_RESPONSE)
+ }
+
+ @Test
+ fun givenAggregatedOrderPrice_whenOpenBIDOrders_thenReturnOrderBookResponseList(): Unit = runBlocking {
+ stubbing(orderRepository) {
+ on {
+ findBySymbolAndDirectionAndStatusSortDescendingByPrice(
+ eq(Valid.ETH_USDT),
+ eq(OrderDirection.BID),
+ eq(1),
+ argThat {
+ this == listOf(
+ OrderStatus.NEW.code,
+ OrderStatus.PARTIALLY_FILLED.code
+ )
+ }
+ )
+ } doReturn Flux.just(Valid.AGGREGATED_ORDER_PRICE_MODEL)
+ }
+
+ val orderBookResponses = marketQueryHandler.openBidOrders(Valid.ETH_USDT, 1)
+
+ assertThat(orderBookResponses).isNotNull
+ assertThat(orderBookResponses.size).isEqualTo(1)
+ assertThat(orderBookResponses.first()).isEqualTo(Valid.ORDER_BOOK_RESPONSE)
+ }
+
+ @Test
+ fun givenOrder_whenLastOrder_thenReturnQueryOrderResponse(): Unit = runBlocking {
+ stubbing(orderRepository) {
+ on {
+ findLastOrderBySymbol(Valid.ETH_USDT)
+ } doReturn Mono.just(Valid.MAKER_ORDER_MODEL)
+ }
+ stubbing(orderStatusRepository) {
+ on {
+ findMostRecentByOUID(Valid.MAKER_ORDER_MODEL.ouid)
+ } doReturn Mono.just(Valid.MAKER_ORDER_STATUS_MODEL)
+ }
+
+ val queryOrderResponse = marketQueryHandler.lastOrder(Valid.ETH_USDT)
+
+ assertThat(queryOrderResponse).isNotNull
+ assertThat(queryOrderResponse).isEqualTo(Valid.MAKER_QUERY_ORDER_RESPONSE)
+ }
+
+ @Test
+ fun givenOrderAndTradeAndSymbolAlias_whenLastPrice_thenPriceTickerResponse(): Unit = runBlocking {
+ stubbing(tradeRepository) {
+ on {
+ findAllGroupBySymbol()
+ } doReturn Flux.just(Valid.TRADE_MODEL)
+ on {
+ findBySymbolGroupBySymbol(Valid.ETH_USDT)
+ } doReturn Flux.just(Valid.TRADE_MODEL)
+ }
+ stubbing(orderRepository) {
+ on {
+ findByOuid(Valid.MAKER_ORDER_MODEL.ouid)
+ } doReturn Mono.just(Valid.MAKER_ORDER_MODEL)
+ }
+ stubbing(symbolMapper) {
+ onBlocking {
+ map(Valid.ETH_USDT)
+ } doReturn "ETHUSDT"
+ }
+
+ val priceTickerResponse = marketQueryHandler.lastPrice(Valid.ETH_USDT)
+
+ assertThat(priceTickerResponse).isNotNull
+ assertThat(priceTickerResponse.size).isEqualTo(1)
+ assertThat(priceTickerResponse.first().symbol).isEqualTo("ETHUSDT")
+ assertThat(priceTickerResponse.first().price).isEqualTo("100000.0")
+ }
+
+ @Test
+ fun givenOrderAndTrade_whenRecentTrades_thenMarketTradeResponseFlow(): Unit = runBlocking {
+ stubbing(tradeRepository) {
+ on {
+ findBySymbolSortDescendingByCreateDate(Valid.ETH_USDT, 1)
+ } doReturn flow {
+ emit(Valid.TRADE_MODEL)
+ }
+ }
+ stubbing(orderRepository) {
+ on {
+ findByOuid(Valid.TRADE_MODEL.makerOuid)
+ } doReturn Mono.just(Valid.MAKER_ORDER_MODEL)
+ on {
+ findByOuid(Valid.TRADE_MODEL.takerOuid)
+ } doReturn Mono.just(Valid.TAKER_ORDER_MODEL)
+ }
+
+ val marketTradeResponses = marketQueryHandler.recentTrades(Valid.ETH_USDT, 1)
+
+ assertThat(marketTradeResponses).isNotNull
+ assertThat(marketTradeResponses.count()).isEqualTo(1)
+ assertThat(marketTradeResponses.first()).isEqualTo(Valid.MARKET_TRADE_RESPONSE)
+ }
+}
+
diff --git a/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/OrderPersisterTest.kt b/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/OrderPersisterTest.kt
new file mode 100644
index 000000000..7c1f45a7c
--- /dev/null
+++ b/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/OrderPersisterTest.kt
@@ -0,0 +1,46 @@
+package co.nilin.opex.api.ports.postgres.impl
+
+import co.nilin.opex.api.ports.postgres.dao.OrderRepository
+import co.nilin.opex.api.ports.postgres.dao.OrderStatusRepository
+import co.nilin.opex.api.ports.postgres.impl.sample.Valid
+import kotlinx.coroutines.runBlocking
+import org.assertj.core.api.Assertions.assertThatNoException
+import org.junit.jupiter.api.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stubbing
+import reactor.core.publisher.Mono
+
+class OrderPersisterTest {
+ private val orderRepository: OrderRepository = mock()
+ private val orderStatusRepository: OrderStatusRepository = mock()
+ private val orderPersister = OrderPersisterImpl(orderRepository, orderStatusRepository)
+
+ @Test
+ fun givenOrderRepo_whenSaveRichOrder_thenSuccess(): Unit = runBlocking {
+ stubbing(orderRepository) {
+ on {
+ save(any())
+ } doReturn Mono.just(Valid.MAKER_ORDER_MODEL)
+ }
+ stubbing(orderStatusRepository) {
+ on {
+ save(any())
+ } doReturn Mono.just(Valid.MAKER_ORDER_STATUS_MODEL)
+ }
+
+ assertThatNoException().isThrownBy { runBlocking { orderPersister.save(Valid.RICH_ORDER) } }
+ }
+
+ @Test
+ fun givenOrderRepo_whenUpdateRichOrder_thenSuccess(): Unit = runBlocking {
+ stubbing(orderStatusRepository) {
+ on {
+ save(any())
+ } doReturn Mono.just(Valid.MAKER_ORDER_STATUS_MODEL)
+ }
+
+ assertThatNoException().isThrownBy { runBlocking { orderPersister.update(Valid.RICH_ORDER_UPDATE) } }
+ }
+}
diff --git a/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/SymbolMapperTest.kt b/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/SymbolMapperTest.kt
new file mode 100644
index 000000000..42666cd75
--- /dev/null
+++ b/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/SymbolMapperTest.kt
@@ -0,0 +1,58 @@
+package co.nilin.opex.api.ports.postgres.impl
+
+import co.nilin.opex.api.ports.postgres.dao.SymbolMapRepository
+import co.nilin.opex.api.ports.postgres.impl.sample.Valid
+import kotlinx.coroutines.runBlocking
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeAll
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stubbing
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class SymbolMapperTest {
+ private val symbolMapRepository: SymbolMapRepository = mock()
+ private val symbolMapper = SymbolMapperImpl(symbolMapRepository)
+
+ @BeforeAll
+ fun setUp() {
+ stubbing(symbolMapRepository) {
+ on {
+ findByAliasKeyAndAlias("binance", "ETHUSDT")
+ } doReturn Mono.just(Valid.SYMBOL_MAP_MODEL)
+ on {
+ findByAliasKeyAndSymbol("binance", Valid.ETH_USDT)
+ } doReturn Mono.just(Valid.SYMBOL_MAP_MODEL)
+ on {
+ findAll()
+ } doReturn Flux.just(Valid.SYMBOL_MAP_MODEL)
+ }
+ }
+
+ @Test
+ fun givenSymbolAlias_whenMapSymbol_thenReturnAlias(): Unit = runBlocking {
+ val alis = symbolMapper.map(Valid.ETH_USDT)
+
+ assertThat(alis).isEqualTo("ETHUSDT")
+ }
+
+ @Test
+ fun givenSymbolAlias_whenUnmapAlias_thenReturnSymbol(): Unit = runBlocking {
+ val symbol = symbolMapper.unmap("ETHUSDT")
+
+ assertThat(symbol).isEqualTo(Valid.ETH_USDT)
+ }
+
+ @Test
+ fun givenSymbolAlias_whenSymbolToAliasMap_thenReturnMap(): Unit = runBlocking {
+ val map = symbolMapper.symbolToAliasMap()
+
+ assertThat(map).isNotNull
+ assertThat(map.size).isEqualTo(1)
+ assertThat(map[Valid.ETH_USDT]).isNotNull()
+ }
+}
diff --git a/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/TradePersisterTest.kt b/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/TradePersisterTest.kt
new file mode 100644
index 000000000..9c16f1638
--- /dev/null
+++ b/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/TradePersisterTest.kt
@@ -0,0 +1,28 @@
+package co.nilin.opex.api.ports.postgres.impl
+
+import co.nilin.opex.api.ports.postgres.dao.TradeRepository
+import co.nilin.opex.api.ports.postgres.impl.sample.Valid
+import kotlinx.coroutines.runBlocking
+import org.assertj.core.api.Assertions.assertThatNoException
+import org.junit.jupiter.api.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stubbing
+import reactor.core.publisher.Mono
+
+class TradePersisterTest {
+ private val tradeRepository: TradeRepository = mock()
+ private val tradePersister = TradePersisterImpl(tradeRepository)
+
+ @Test
+ fun givenTradeRepo_whenSaveRichTrade_thenSuccess(): Unit = runBlocking {
+ stubbing(tradeRepository) {
+ on {
+ save(any())
+ } doReturn Mono.just(Valid.TRADE_MODEL)
+ }
+
+ assertThatNoException().isThrownBy { runBlocking { tradePersister.save(Valid.RICH_TRADE) } }
+ }
+}
diff --git a/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/UserQueryHandlerTest.kt b/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/UserQueryHandlerTest.kt
new file mode 100644
index 000000000..22f6b9c26
--- /dev/null
+++ b/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/UserQueryHandlerTest.kt
@@ -0,0 +1,130 @@
+package co.nilin.opex.api.ports.postgres.impl
+
+import co.nilin.opex.api.core.inout.OrderStatus
+import co.nilin.opex.api.ports.postgres.dao.OrderRepository
+import co.nilin.opex.api.ports.postgres.dao.OrderStatusRepository
+import co.nilin.opex.api.ports.postgres.dao.TradeRepository
+import co.nilin.opex.api.ports.postgres.impl.sample.Valid
+import kotlinx.coroutines.flow.count
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.runBlocking
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.mockito.kotlin.*
+import reactor.core.publisher.Mono
+
+class UserQueryHandlerTest {
+ private val orderRepository: OrderRepository = mock()
+ private val tradeRepository: TradeRepository = mock()
+ private val orderStatusRepository: OrderStatusRepository = mock()
+ private val userQueryHandler = UserQueryHandlerImpl(orderRepository, tradeRepository, orderStatusRepository)
+
+ @Test
+ fun givenOrder_whenAllOrders_thenReturnQueryOrderResponseList(): Unit = runBlocking {
+ stubbing(orderRepository) {
+ on {
+ findByUuidAndSymbolAndTimeBetween(
+ Valid.PRINCIPAL.name,
+ Valid.ALL_ORDER_REQUEST.symbol,
+ Valid.ALL_ORDER_REQUEST.startTime,
+ Valid.ALL_ORDER_REQUEST.endTime
+ )
+ } doReturn flow {
+ emit(Valid.MAKER_ORDER_MODEL)
+ }
+ }
+ stubbing(orderStatusRepository) {
+ on {
+ findMostRecentByOUID(Valid.MAKER_ORDER_MODEL.ouid)
+ } doReturn Mono.just(Valid.MAKER_ORDER_STATUS_MODEL)
+ }
+
+ val queryOrderResponses = userQueryHandler.allOrders(Valid.PRINCIPAL, Valid.ALL_ORDER_REQUEST)
+
+ assertThat(queryOrderResponses).isNotNull
+ assertThat(queryOrderResponses.count()).isEqualTo(1)
+ assertThat(queryOrderResponses.first()).isEqualTo(Valid.MAKER_QUERY_ORDER_RESPONSE)
+ }
+
+ @Test
+ fun givenOrderAndTrade_whenAllTrades_thenTradeResponseList(): Unit = runBlocking {
+ stubbing(tradeRepository) {
+ on {
+ findByUuidAndSymbolAndTimeBetweenAndTradeIdGreaterThan(
+ Valid.PRINCIPAL.name,
+ Valid.TRADE_REQUEST.symbol,
+ 1,
+ Valid.TRADE_REQUEST.startTime,
+ Valid.TRADE_REQUEST.endTime
+ )
+ } doReturn flow {
+ emit(Valid.TRADE_MODEL)
+ }
+ }
+ stubbing(orderRepository) {
+ on {
+ findByOuid(Valid.TRADE_MODEL.makerOuid)
+ } doReturn Mono.just(Valid.MAKER_ORDER_MODEL)
+ on {
+ findByOuid(Valid.TRADE_MODEL.takerOuid)
+ } doReturn Mono.just(Valid.TAKER_ORDER_MODEL)
+ }
+
+ val tradeResponses = userQueryHandler.allTrades(Valid.PRINCIPAL, Valid.TRADE_REQUEST)
+
+ assertThat(tradeResponses).isNotNull
+ assertThat(tradeResponses.count()).isEqualTo(1)
+ }
+
+ @Test
+ fun givenOrder_whenOpenOrders_thenReturnQueryOrderResponseList(): Unit = runBlocking {
+ stubbing(orderRepository) {
+ on {
+ findByUuidAndSymbolAndStatus(
+ eq(Valid.PRINCIPAL.name),
+ eq(Valid.ETH_USDT),
+ argThat {
+ this == listOf(
+ OrderStatus.NEW.code,
+ OrderStatus.PARTIALLY_FILLED.code
+ )
+ }
+ )
+ } doReturn flow {
+ emit(Valid.MAKER_ORDER_MODEL)
+ }
+ }
+ stubbing(orderStatusRepository) {
+ on {
+ findMostRecentByOUID(Valid.MAKER_ORDER_MODEL.ouid)
+ } doReturn Mono.just(Valid.MAKER_ORDER_STATUS_MODEL)
+ }
+
+ val queryOrderResponses = userQueryHandler.openOrders(Valid.PRINCIPAL, Valid.ETH_USDT)
+
+ assertThat(queryOrderResponses).isNotNull
+ assertThat(queryOrderResponses.count()).isEqualTo(1)
+ }
+
+ @Test
+ fun givenOrder_whenQueryOrder_thenReturnQueryOrderResponse(): Unit = runBlocking {
+ stubbing(orderRepository) {
+ on {
+ findBySymbolAndClientOrderId(Valid.ETH_USDT, "2")
+ } doReturn Mono.just(Valid.MAKER_ORDER_MODEL)
+ on {
+ findBySymbolAndOrderId(Valid.ETH_USDT, 1)
+ } doReturn Mono.just(Valid.MAKER_ORDER_MODEL)
+ }
+ stubbing(orderStatusRepository) {
+ on {
+ findMostRecentByOUID(Valid.MAKER_ORDER_MODEL.ouid)
+ } doReturn Mono.just(Valid.MAKER_ORDER_STATUS_MODEL)
+ }
+
+ val queryOrderResponse = userQueryHandler.queryOrder(Valid.PRINCIPAL, Valid.QUERY_ORDER_REQUEST)
+
+ assertThat(queryOrderResponse).isNotNull
+ }
+}
diff --git a/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/sample/Samples.kt b/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/sample/Samples.kt
new file mode 100644
index 000000000..ce90c5b40
--- /dev/null
+++ b/api/api-ports/api-persister-postgres/src/test/kotlin/co/nilin/opex/api/ports/postgres/impl/sample/Samples.kt
@@ -0,0 +1,236 @@
+package co.nilin.opex.api.ports.postgres.impl.sample
+
+import co.nilin.opex.api.core.event.RichOrder
+import co.nilin.opex.api.core.event.RichOrderUpdate
+import co.nilin.opex.api.core.event.RichTrade
+import co.nilin.opex.api.core.inout.*
+import co.nilin.opex.api.ports.postgres.model.OrderModel
+import co.nilin.opex.api.ports.postgres.model.OrderStatusModel
+import co.nilin.opex.api.ports.postgres.model.SymbolMapModel
+import co.nilin.opex.api.ports.postgres.model.TradeModel
+import co.nilin.opex.api.ports.postgres.util.isWorking
+import java.math.BigDecimal
+import java.security.Principal
+import java.time.LocalDateTime
+import java.time.ZoneOffset
+import java.util.*
+
+object Valid {
+ private const val TIMESTAMP = 1653125840L
+ private val CREATE_DATE: LocalDateTime = LocalDateTime.ofEpochSecond(TIMESTAMP, 0, ZoneOffset.UTC)
+ private val UPDATE_DATE: LocalDateTime = LocalDateTime.ofEpochSecond(TIMESTAMP + 180, 0, ZoneOffset.UTC)
+ private val FROM_DATE: LocalDateTime = LocalDateTime.ofEpochSecond(TIMESTAMP - 600, 0, ZoneOffset.UTC)
+ private val TO_DATE: LocalDateTime = LocalDateTime.ofEpochSecond(TIMESTAMP + 600, 0, ZoneOffset.UTC)
+
+ const val ETH_USDT = "ETH_USDT"
+
+ val PRINCIPAL = Principal { "98c7ca9b-2d9c-46dd-afa8-b0cd2f52a97c" }
+
+ val MAKER_ORDER_MODEL = OrderModel(
+ 1,
+ "f1167d30-ccc0-4f86-ab5d-dd24aa3250df",
+ PRINCIPAL.name,
+ null, // Binance
+ ETH_USDT,
+ 1, // MatchingEngine ID
+ 0.01, // Calculated?
+ 0.01, // Calculated?
+ 0.0001,
+ 0.01,
+ "1",
+ OrderDirection.ASK,
+ MatchConstraint.GTC,
+ MatchingOrderType.LIMIT_ORDER,
+ 100000.0,
+ 0.001,
+ 100000.0 * 0.001,
+ CREATE_DATE,
+ UPDATE_DATE
+ )
+
+ val TAKER_ORDER_MODEL = OrderModel(
+ 2,
+ "157b9b4a-cc66-43b9-b30b-40a8b66ea6aa",
+ PRINCIPAL.name,
+ null,
+ ETH_USDT,
+ 2,
+ 0.01,
+ 0.01,
+ 0.0001,
+ 0.01,
+ "1",
+ OrderDirection.BID,
+ MatchConstraint.GTC,
+ MatchingOrderType.LIMIT_ORDER,
+ 100000.0,
+ 0.001,
+ 100000.0 * 0.01,
+ CREATE_DATE,
+ UPDATE_DATE
+ )
+
+ val MAKER_ORDER_STATUS_MODEL = OrderStatusModel(
+ MAKER_ORDER_MODEL.ouid,
+ 0.0, // Filled amount
+ 0.0, // --> See accountant
+ OrderStatus.FILLED.code,
+ OrderStatus.FILLED.orderOfAppearance,
+ CREATE_DATE
+ )
+
+ val TAKER_ORDER_STATUS_MODEL = OrderStatusModel(
+ TAKER_ORDER_MODEL.ouid,
+ 0.0, // Filled amount
+ 0.0, // --> See accountant
+ OrderStatus.FILLED.code,
+ OrderStatus.FILLED.orderOfAppearance,
+ CREATE_DATE
+ )
+
+ val SYMBOL_MAP_MODEL = SymbolMapModel(
+ 1,
+ ETH_USDT,
+ "binance",
+ ETH_USDT.replace("_", "")
+ )
+
+ val TRADE_MODEL = TradeModel(
+ 1,
+ 1,
+ ETH_USDT,
+ 0.001, // Minimum of orders quantities
+ 100000.0,
+ 100000.0,
+ 0.001, // Calculated
+ 0.001, // Calculated
+ "ETH",
+ "USDT",
+ UPDATE_DATE,
+ MAKER_ORDER_MODEL.ouid,
+ TAKER_ORDER_MODEL.ouid,
+ PRINCIPAL.name,
+ PRINCIPAL.name,
+ CREATE_DATE
+ )
+
+ val MAKER_QUERY_ORDER_RESPONSE = QueryOrderResponse(
+ ETH_USDT,
+ MAKER_ORDER_MODEL.ouid,
+ 1,
+ -1, // Binance
+ "", // Binance
+ BigDecimal.valueOf(100000.0),
+ BigDecimal.valueOf(0.001),
+ BigDecimal.valueOf(0.0),
+ BigDecimal.valueOf(0.0),
+ OrderStatus.FILLED,
+ TimeInForce.GTC,
+ OrderType.LIMIT,
+ OrderSide.SELL,
+ null,
+ null,
+ Date.from(CREATE_DATE.toInstant(ZoneOffset.UTC)),
+ Date.from(UPDATE_DATE.toInstant(ZoneOffset.UTC)),
+ OrderStatus.FILLED.isWorking(),
+ BigDecimal.valueOf(100000.0 * 0.001)
+ )
+
+ val AGGREGATED_ORDER_PRICE_MODEL = AggregatedOrderPriceModel(
+ 100000.0,
+ 0.001
+ )
+
+ val ORDER_BOOK_RESPONSE = OrderBookResponse(
+ AGGREGATED_ORDER_PRICE_MODEL.price!!.toBigDecimal(),
+ AGGREGATED_ORDER_PRICE_MODEL.quantity!!.toBigDecimal()
+ )
+
+ val RICH_ORDER = RichOrder(
+ null,
+ ETH_USDT,
+ MAKER_ORDER_MODEL.ouid,
+ PRINCIPAL.name,
+ "1",
+ BigDecimal.valueOf(0.01),
+ BigDecimal.valueOf(0.01),
+ BigDecimal.valueOf(0.0001),
+ BigDecimal.valueOf(0.01),
+ OrderDirection.ASK,
+ MatchConstraint.GTC,
+ MatchingOrderType.LIMIT_ORDER,
+ BigDecimal.valueOf(1000001),
+ BigDecimal.valueOf(0.01),
+ BigDecimal.valueOf(0),
+ BigDecimal.valueOf(0),
+ BigDecimal.valueOf(0),
+ 0
+ )
+
+ val RICH_ORDER_UPDATE = RichOrderUpdate(
+ MAKER_ORDER_MODEL.ouid,
+ BigDecimal.valueOf(1000001),
+ BigDecimal.valueOf(0.01),
+ BigDecimal.valueOf(0.08),
+ OrderStatus.PARTIALLY_FILLED
+ )
+
+ val RICH_TRADE = RichTrade(
+ 1,
+ ETH_USDT,
+ MAKER_ORDER_MODEL.ouid,
+ PRINCIPAL.name,
+ 1,
+ OrderDirection.ASK,
+ BigDecimal.valueOf(100000),
+ BigDecimal.valueOf(0.01),
+ BigDecimal.valueOf(0),
+ BigDecimal.valueOf(0),
+ BigDecimal.valueOf(0),
+ "ETH",
+ TAKER_ORDER_MODEL.ouid,
+ PRINCIPAL.name,
+ 2,
+ OrderDirection.ASK,
+ BigDecimal.valueOf(100000),
+ BigDecimal.valueOf(0.01),
+ BigDecimal.valueOf(0),
+ BigDecimal.valueOf(0),
+ BigDecimal.valueOf(0),
+ "USDT",
+ BigDecimal.valueOf(0),
+ CREATE_DATE
+ )
+
+ val ALL_ORDER_REQUEST = AllOrderRequest(
+ ETH_USDT,
+ Date.from(FROM_DATE.toInstant(ZoneOffset.UTC)),
+ Date.from(TO_DATE.toInstant(ZoneOffset.UTC)),
+ 500
+ )
+
+ val TRADE_REQUEST = TradeRequest(
+ ETH_USDT,
+ 1,
+ Date.from(FROM_DATE.toInstant(ZoneOffset.UTC)),
+ Date.from(TO_DATE.toInstant(ZoneOffset.UTC)),
+ 500
+ )
+
+ val MARKET_TRADE_RESPONSE = MarketTradeResponse(
+ ETH_USDT,
+ 1,
+ BigDecimal.valueOf(100000.0),
+ BigDecimal.valueOf(0.001),
+ BigDecimal.valueOf(100000.0 * 0.001),
+ Date.from(CREATE_DATE.toInstant(ZoneOffset.UTC)),
+ true,
+ MAKER_ORDER_MODEL.direction == OrderDirection.BID
+ )
+
+ val QUERY_ORDER_REQUEST = QueryOrderRequest(
+ ETH_USDT,
+ 1,
+ "2"
+ )
+}
diff --git a/bc-gateway/bc-gateway-core/pom.xml b/bc-gateway/bc-gateway-core/pom.xml
index 619be8c6d..e82998eae 100644
--- a/bc-gateway/bc-gateway-core/pom.xml
+++ b/bc-gateway/bc-gateway-core/pom.xml
@@ -14,10 +14,6 @@
bc-gateway-core
Blockchain gateway core of Opex
-
- 4.0.0
-
-
org.jetbrains.kotlin
@@ -47,8 +43,6 @@
org.mockito.kotlin
mockito-kotlin
- ${mockito-kotlin.version}
- test
co.nilin.opex.utility.error
diff --git a/matching-gateway/matching-gateway-app/pom.xml b/matching-gateway/matching-gateway-app/pom.xml
index 583fe0d38..89c9fbcef 100644
--- a/matching-gateway/matching-gateway-app/pom.xml
+++ b/matching-gateway/matching-gateway-app/pom.xml
@@ -73,6 +73,10 @@
co.nilin.opex.utility.log
logging-handler
+
+ org.mockito.kotlin
+ mockito-kotlin
+
diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt
index 3647c7f21..dff49032a 100644
--- a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt
+++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt
@@ -74,4 +74,4 @@ class OrderService(
val event = CancelOrderEvent(request.ouid, request.uuid, request.orderId, Pair(symbols[0], symbols[1]))
return eventSubmitter.submit(event)
}
-}
\ No newline at end of file
+}
diff --git a/matching-gateway/matching-gateway-app/src/test/kotlin/co/nilin/opex/matching/gateway/app/service/OrderServiceTest.kt b/matching-gateway/matching-gateway-app/src/test/kotlin/co/nilin/opex/matching/gateway/app/service/OrderServiceTest.kt
new file mode 100644
index 000000000..60d75b6c4
--- /dev/null
+++ b/matching-gateway/matching-gateway-app/src/test/kotlin/co/nilin/opex/matching/gateway/app/service/OrderServiceTest.kt
@@ -0,0 +1,480 @@
+package co.nilin.opex.matching.gateway.app.service
+
+import co.nilin.opex.matching.engine.core.model.MatchConstraint
+import co.nilin.opex.matching.engine.core.model.OrderDirection
+import co.nilin.opex.matching.engine.core.model.OrderType
+import co.nilin.opex.matching.gateway.app.inout.CancelOrderRequest
+import co.nilin.opex.matching.gateway.app.inout.CreateOrderRequest
+import co.nilin.opex.matching.gateway.app.inout.PairConfig
+import co.nilin.opex.matching.gateway.app.inout.PairFeeConfig
+import co.nilin.opex.matching.gateway.app.spi.AccountantApiProxy
+import co.nilin.opex.matching.gateway.app.spi.PairConfigLoader
+import co.nilin.opex.matching.gateway.ports.kafka.submitter.inout.OrderSubmitResult
+import co.nilin.opex.matching.gateway.ports.kafka.submitter.service.EventSubmitter
+import co.nilin.opex.matching.gateway.ports.kafka.submitter.service.KafkaHealthIndicator
+import co.nilin.opex.matching.gateway.ports.kafka.submitter.service.OrderSubmitter
+import kotlinx.coroutines.runBlocking
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.junit.jupiter.api.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stubbing
+import java.math.BigDecimal
+
+private class OrderServiceTest {
+ private val accountantApiProxy: AccountantApiProxy = mock()
+ private val orderSubmitter: OrderSubmitter = mock()
+ private val eventSubmitter: EventSubmitter = mock()
+ private val pairConfigLoader: PairConfigLoader = mock()
+ private val kafkaHealthIndicator: KafkaHealthIndicator = mock()
+ private val orderService: OrderService = OrderService(
+ accountantApiProxy,
+ orderSubmitter,
+ eventSubmitter,
+ pairConfigLoader,
+ kafkaHealthIndicator
+ )
+
+ @Test
+ fun givenPair_whenSubmitNewOrder_thenOrderSubmitResult(): Unit = runBlocking {
+ val pairConfig = PairConfig("ETH_USDT", "ETH", "USDT", 0.01, 0.0001)
+ val order = CreateOrderRequest(
+ "a2930d06-0c84-4448-bff7-65134184bb1d",
+ "ETH_USDT",
+ BigDecimal.valueOf(100000),
+ BigDecimal.valueOf(0.001),
+ OrderDirection.ASK,
+ MatchConstraint.GTC,
+ OrderType.LIMIT_ORDER
+ )
+ stubbing(pairConfigLoader) {
+ onBlocking { load("ETH_USDT", OrderDirection.ASK, "") } doReturn PairFeeConfig(
+ pairConfig,
+ "ASK",
+ "",
+ 0.01,
+ 0.01
+ )
+ }
+ stubbing(accountantApiProxy) {
+ onBlocking {
+ canCreateOrder(order.uuid!!, "ETH", order.quantity)
+ } doReturn true
+ }
+ stubbing(orderSubmitter) {
+ onBlocking {
+ submit(any())
+ } doReturn OrderSubmitResult(null)
+ }
+ stubbing(kafkaHealthIndicator) {
+ on { isHealthy } doReturn true
+ }
+
+ val orderSubmitResult = orderService.submitNewOrder(order)
+
+ assertThat(orderSubmitResult).isNotNull
+ }
+
+ @Test
+ fun givenPair_whenSubmitNewOrderByInvalidSymbol_thenThrow(): Unit = runBlocking {
+ val pairConfig = PairConfig("ETH_USDT", "ETH", "USDT", 0.01, 0.0001)
+ val order = CreateOrderRequest(
+ "a2930d06-0c84-4448-bff7-65134184bb1d",
+ "BTC_USDT",
+ BigDecimal.valueOf(100000),
+ BigDecimal.valueOf(0.001),
+ OrderDirection.ASK,
+ MatchConstraint.GTC,
+ OrderType.LIMIT_ORDER
+ )
+ stubbing(pairConfigLoader) {
+ onBlocking { load("ETH_USDT", OrderDirection.ASK, "") } doReturn PairFeeConfig(
+ pairConfig,
+ "ASK",
+ "",
+ 0.01,
+ 0.01
+ )
+ }
+ stubbing(accountantApiProxy) {
+ onBlocking {
+ canCreateOrder(order.uuid!!, "ETH", order.quantity)
+ } doReturn true
+ }
+ stubbing(orderSubmitter) {
+ onBlocking {
+ submit(any())
+ } doReturn OrderSubmitResult(null)
+ }
+ stubbing(kafkaHealthIndicator) {
+ on { isHealthy } doReturn true
+ }
+
+ assertThatThrownBy { runBlocking { orderService.submitNewOrder(order) } }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenPair_whenSubmitNewOrderByASKAndInvalidPrice_thenThrow(): Unit = runBlocking {
+ val pairConfig = PairConfig("ETH_USDT", "ETH", "USDT", 0.01, 0.0001)
+ val order = CreateOrderRequest(
+ "a2930d06-0c84-4448-bff7-65134184bb1d",
+ "ETH_USDT",
+ BigDecimal.valueOf(-100000),
+ BigDecimal.valueOf(0.001),
+ OrderDirection.ASK,
+ MatchConstraint.GTC,
+ OrderType.LIMIT_ORDER
+ )
+ stubbing(pairConfigLoader) {
+ onBlocking { load("ETH_USDT", OrderDirection.ASK, "") } doReturn PairFeeConfig(
+ pairConfig,
+ "ASK",
+ "",
+ 0.01,
+ 0.01
+ )
+ }
+ stubbing(accountantApiProxy) {
+ onBlocking {
+ canCreateOrder(order.uuid!!, "ETH", order.quantity)
+ } doReturn true
+ }
+ stubbing(orderSubmitter) {
+ onBlocking {
+ submit(any())
+ } doReturn OrderSubmitResult(null)
+ }
+ stubbing(kafkaHealthIndicator) {
+ on { isHealthy } doReturn true
+ }
+
+ assertThatThrownBy { runBlocking { orderService.submitNewOrder(order) } }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenPair_whenSubmitNewOrderByASKAndInvalidQuantity_thenThrow(): Unit = runBlocking {
+ val pairConfig = PairConfig("ETH_USDT", "ETH", "USDT", 0.01, 0.0001)
+ val order = CreateOrderRequest(
+ "a2930d06-0c84-4448-bff7-65134184bb1d",
+ "ETH_USDT",
+ BigDecimal.valueOf(100000),
+ BigDecimal.valueOf(-0.001),
+ OrderDirection.ASK,
+ MatchConstraint.GTC,
+ OrderType.LIMIT_ORDER
+ )
+ stubbing(pairConfigLoader) {
+ onBlocking { load("ETH_USDT", OrderDirection.ASK, "") } doReturn PairFeeConfig(
+ pairConfig,
+ "ASK",
+ "",
+ 0.01,
+ 0.01
+ )
+ }
+ stubbing(accountantApiProxy) {
+ onBlocking {
+ canCreateOrder(order.uuid!!, "ETH", order.quantity)
+ } doReturn true
+ }
+ stubbing(orderSubmitter) {
+ onBlocking {
+ submit(any())
+ } doReturn OrderSubmitResult(null)
+ }
+ stubbing(kafkaHealthIndicator) {
+ on { isHealthy } doReturn true
+ }
+
+ assertThatThrownBy { runBlocking { orderService.submitNewOrder(order) } }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenPair_whenSubmitNewOrderByASKAndInvalidLevel_thenThrow(): Unit = runBlocking {
+ val pairConfig = PairConfig("ETH_USDT", "ETH", "USDT", 0.01, 0.0001)
+ val order = CreateOrderRequest(
+ "a2930d06-0c84-4448-bff7-65134184bb1d",
+ "ETH_USDT",
+ BigDecimal.valueOf(100000),
+ BigDecimal.valueOf(0.001),
+ OrderDirection.ASK,
+ MatchConstraint.GTC,
+ OrderType.LIMIT_ORDER
+ )
+ stubbing(pairConfigLoader) {
+ onBlocking { load("ETH_USDT", OrderDirection.ASK, "1") } doReturn PairFeeConfig(
+ pairConfig,
+ "ASK",
+ "1",
+ 0.01,
+ 0.01
+ )
+ }
+ stubbing(accountantApiProxy) {
+ onBlocking {
+ canCreateOrder(order.uuid!!, "ETH", order.quantity)
+ } doReturn true
+ }
+ stubbing(orderSubmitter) {
+ onBlocking {
+ submit(any())
+ } doReturn OrderSubmitResult(null)
+ }
+ stubbing(kafkaHealthIndicator) {
+ on { isHealthy } doReturn true
+ }
+
+ assertThatThrownBy { runBlocking { orderService.submitNewOrder(order) } }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenPair_whenSubmitNewOrderByBID_thenOrderSubmitResult(): Unit = runBlocking {
+ val pairConfig = PairConfig("ETH_USDT", "ETH", "USDT", 0.01, 0.0001)
+ val order = CreateOrderRequest(
+ "a2930d06-0c84-4448-bff7-65134184bb1d",
+ "ETH_USDT",
+ BigDecimal.valueOf(100000),
+ BigDecimal.valueOf(0.001),
+ OrderDirection.BID,
+ MatchConstraint.GTC,
+ OrderType.LIMIT_ORDER
+ )
+ stubbing(pairConfigLoader) {
+ onBlocking { load("ETH_USDT", OrderDirection.BID, "") } doReturn PairFeeConfig(
+ pairConfig,
+ "BID",
+ "",
+ 0.01,
+ 0.01
+ )
+ }
+ stubbing(accountantApiProxy) {
+ onBlocking {
+ canCreateOrder(order.uuid!!, "USDT", order.quantity * order.price)
+ } doReturn true
+ }
+ stubbing(orderSubmitter) {
+ onBlocking {
+ submit(any())
+ } doReturn OrderSubmitResult(null)
+ }
+ stubbing(kafkaHealthIndicator) {
+ on { isHealthy } doReturn true
+ }
+
+ val orderSubmitResult = orderService.submitNewOrder(order)
+
+ assertThat(orderSubmitResult).isNotNull
+ }
+
+ @Test
+ fun givenPair_whenSubmitNewOrderByBIDAndInvalidSymbol_thenThrow(): Unit = runBlocking {
+ val pairConfig = PairConfig("ETH_USDT", "ETH", "USDT", 0.01, 0.0001)
+ val order = CreateOrderRequest(
+ "a2930d06-0c84-4448-bff7-65134184bb1d",
+ "BTC_USDT",
+ BigDecimal.valueOf(100000),
+ BigDecimal.valueOf(0.001),
+ OrderDirection.BID,
+ MatchConstraint.GTC,
+ OrderType.LIMIT_ORDER
+ )
+ stubbing(pairConfigLoader) {
+ onBlocking { load("ETH_USDT", OrderDirection.BID, "") } doReturn PairFeeConfig(
+ pairConfig,
+ "BID",
+ "",
+ 0.01,
+ 0.01
+ )
+ }
+ stubbing(accountantApiProxy) {
+ onBlocking {
+ canCreateOrder(order.uuid!!, "USDT", order.quantity * order.price)
+ } doReturn true
+ }
+ stubbing(orderSubmitter) {
+ onBlocking {
+ submit(any())
+ } doReturn OrderSubmitResult(null)
+ }
+ stubbing(kafkaHealthIndicator) {
+ on { isHealthy } doReturn true
+ }
+
+ assertThatThrownBy { runBlocking { orderService.submitNewOrder(order) } }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenPair_whenSubmitNewOrderByBIDAndNotExistOwner_thenThrow(): Unit = runBlocking {
+ val pairConfig = PairConfig("ETH_USDT", "ETH", "USDT", 0.01, 0.0001)
+ val order = CreateOrderRequest(
+ "55408c0a-ed53-42d1-b5ee-b2edf531b9d5",
+ "ETH_USDT",
+ BigDecimal.valueOf(100000),
+ BigDecimal.valueOf(0.001),
+ OrderDirection.BID,
+ MatchConstraint.GTC,
+ OrderType.LIMIT_ORDER
+ )
+ stubbing(pairConfigLoader) {
+ onBlocking { load("ETH_USDT", OrderDirection.BID, "") } doReturn PairFeeConfig(
+ pairConfig,
+ "BID",
+ "",
+ 0.01,
+ 0.01
+ )
+ }
+ stubbing(accountantApiProxy) {
+ onBlocking {
+ canCreateOrder(order.uuid!!, "USDT", order.quantity * order.price)
+ } doReturn true
+ }
+ stubbing(orderSubmitter) {
+ onBlocking {
+ submit(any())
+ } doReturn OrderSubmitResult(null)
+ }
+ stubbing(kafkaHealthIndicator) {
+ on { isHealthy } doReturn true
+ }
+
+ assertThatThrownBy { runBlocking { orderService.submitNewOrder(order) } }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenPair_whenSubmitNewOrderByBIDAndInvalidPrice_thenThrow(): Unit = runBlocking {
+ val pairConfig = PairConfig("ETH_USDT", "ETH", "USDT", 0.01, 0.0001)
+ val order = CreateOrderRequest(
+ "a2930d06-0c84-4448-bff7-65134184bb1d",
+ "ETH_USDT",
+ BigDecimal.valueOf(-100000),
+ BigDecimal.valueOf(0.001),
+ OrderDirection.BID,
+ MatchConstraint.GTC,
+ OrderType.LIMIT_ORDER
+ )
+ stubbing(pairConfigLoader) {
+ onBlocking { load("ETH_USDT", OrderDirection.BID, "") } doReturn PairFeeConfig(
+ pairConfig,
+ "BID",
+ "",
+ 0.01,
+ 0.01
+ )
+ }
+ stubbing(accountantApiProxy) {
+ onBlocking {
+ canCreateOrder(order.uuid!!, "USDT", order.quantity * order.price)
+ } doReturn true
+ }
+ stubbing(orderSubmitter) {
+ onBlocking {
+ submit(any())
+ } doReturn OrderSubmitResult(null)
+ }
+ stubbing(kafkaHealthIndicator) {
+ on { isHealthy } doReturn true
+ }
+
+ assertThatThrownBy { runBlocking { orderService.submitNewOrder(order) } }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenPair_whenSubmitNewOrderByBIDAndInvalidQuantity_thenThrow(): Unit = runBlocking {
+ val pairConfig = PairConfig("ETH_USDT", "ETH", "USDT", 0.01, 0.0001)
+ val order = CreateOrderRequest(
+ "a2930d06-0c84-4448-bff7-65134184bb1d",
+ "ETH_USDT",
+ BigDecimal.valueOf(100000),
+ BigDecimal.valueOf(-0.001),
+ OrderDirection.BID,
+ MatchConstraint.GTC,
+ OrderType.LIMIT_ORDER
+ )
+ stubbing(pairConfigLoader) {
+ onBlocking { load("ETH_USDT", OrderDirection.BID, "") } doReturn PairFeeConfig(
+ pairConfig,
+ "BID",
+ "",
+ 0.01,
+ 0.01
+ )
+ }
+ stubbing(accountantApiProxy) {
+ onBlocking {
+ canCreateOrder(order.uuid!!, "USDT", order.quantity * order.price)
+ } doReturn true
+ }
+ stubbing(orderSubmitter) {
+ onBlocking {
+ submit(any())
+ } doReturn OrderSubmitResult(null)
+ }
+ stubbing(kafkaHealthIndicator) {
+ on { isHealthy } doReturn true
+ }
+
+ assertThatThrownBy { runBlocking { orderService.submitNewOrder(order) } }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenPair_whenSubmitNewOrderByBIDAndInvalidLevel_thenThrow(): Unit = runBlocking {
+ val pairConfig = PairConfig("ETH_USDT", "ETH", "USDT", 0.01, 0.0001)
+ val order = CreateOrderRequest(
+ "a2930d06-0c84-4448-bff7-65134184bb1d",
+ "ETH_USDT",
+ BigDecimal.valueOf(100000),
+ BigDecimal.valueOf(0.001),
+ OrderDirection.BID,
+ MatchConstraint.GTC,
+ OrderType.LIMIT_ORDER
+ )
+ stubbing(pairConfigLoader) {
+ onBlocking { load("ETH_USDT", OrderDirection.BID, "1") } doReturn PairFeeConfig(
+ pairConfig,
+ "BID",
+ "1",
+ 0.01,
+ 0.01
+ )
+ }
+ stubbing(accountantApiProxy) {
+ onBlocking {
+ canCreateOrder(order.uuid!!, "USDT", order.quantity * order.price)
+ } doReturn true
+ }
+ stubbing(orderSubmitter) {
+ onBlocking {
+ submit(any())
+ } doReturn OrderSubmitResult(null)
+ }
+ stubbing(kafkaHealthIndicator) {
+ on { isHealthy } doReturn true
+ }
+
+ assertThatThrownBy { runBlocking { orderService.submitNewOrder(order) } }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenEventSubmitter_whenCancelOrder_thenOrderSubmitResult(): Unit = runBlocking {
+ val order = CancelOrderRequest(
+ "edee8090-62d9-4929-b70d-5b97de0c29eb",
+ "a2930d06-0c84-4448-bff7-65134184bb1d",
+ 1,
+ "ETH_USDT"
+ )
+ stubbing(eventSubmitter) {
+ onBlocking {
+ submit(any())
+ } doReturn OrderSubmitResult(null)
+ }
+
+ val orderSubmitResult = orderService.cancelOrder(order)
+
+ assertThat(orderSubmitResult).isNotNull
+ }
+}
diff --git a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/service/KafkaHealthIndicator.kt b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/service/KafkaHealthIndicator.kt
index 38f0f8e2f..f6e1d91b4 100644
--- a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/service/KafkaHealthIndicator.kt
+++ b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/service/KafkaHealthIndicator.kt
@@ -12,12 +12,13 @@ class KafkaHealthIndicator(private val adminClient: AdminClient) {
private val logger = LoggerFactory.getLogger(KafkaHealthIndicator::class.java)
private val options = DescribeClusterOptions().timeoutMs(1000)
private val healthyNodeSize = 3
- var isHealthy = false
- protected set
+ private var pIsHealthy = false
+ val isHealthy
+ get() = pIsHealthy
@Scheduled(fixedDelay = 5000, initialDelay = 5000)
fun check() {
- isHealthy = try {
+ pIsHealthy = try {
val description = adminClient.describeCluster(options)
if (description.nodes().get().size < healthyNodeSize)
throw IllegalStateException("Insufficient nodes")
@@ -27,5 +28,4 @@ class KafkaHealthIndicator(private val adminClient: AdminClient) {
false
}
}
-
}
diff --git a/pom.xml b/pom.xml
index 9e524604f..a3c5afdcc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,7 +16,7 @@
1.6.0
2.6.2
2021.0.0
- false
+ true
@@ -59,12 +59,26 @@
${spring.version}
test
+
+ org.mockito
+ mockito-core
+
+
+ org.mockito
+ mockito-junit-jupiter
+
org.junit.vintage
junit-vintage-engine
+
+ org.mockito.kotlin
+ mockito-kotlin
+ 4.0.0
+ test
+
diff --git a/wallet/wallet-core/pom.xml b/wallet/wallet-core/pom.xml
index 507cf69d3..3414a8f56 100644
--- a/wallet/wallet-core/pom.xml
+++ b/wallet/wallet-core/pom.xml
@@ -28,5 +28,14 @@
spring-tx
provided
+
+ org.mockito.kotlin
+ mockito-kotlin
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+ test
+
diff --git a/wallet/wallet-core/src/test/kotlin/co/nilin/opex/wallet/core/service/TransferServiceTest.kt b/wallet/wallet-core/src/test/kotlin/co/nilin/opex/wallet/core/service/TransferServiceTest.kt
new file mode 100644
index 000000000..d222a35a9
--- /dev/null
+++ b/wallet/wallet-core/src/test/kotlin/co/nilin/opex/wallet/core/service/TransferServiceTest.kt
@@ -0,0 +1,571 @@
+package co.nilin.opex.wallet.core.service
+
+import co.nilin.opex.wallet.core.inout.TransferCommand
+import co.nilin.opex.wallet.core.model.Amount
+import co.nilin.opex.wallet.core.model.Currency
+import co.nilin.opex.wallet.core.model.Wallet
+import co.nilin.opex.wallet.core.model.WalletOwner
+import co.nilin.opex.wallet.core.spi.TransactionManager
+import co.nilin.opex.wallet.core.spi.WalletListener
+import co.nilin.opex.wallet.core.spi.WalletManager
+import co.nilin.opex.wallet.core.spi.WalletOwnerManager
+import kotlinx.coroutines.runBlocking
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.junit.jupiter.api.Test
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.kotlin.*
+import java.math.BigDecimal
+
+private class TransferServiceTest {
+ private val walletOwnerManager: WalletOwnerManager = mock()
+ private val walletManager: WalletManager = mock()
+ private val walletListener: WalletListener = mock()
+ private val transactionManager: TransactionManager = mock()
+ private val transferService: TransferService =
+ TransferService(walletManager, walletListener, walletOwnerManager, transactionManager)
+
+ private val currency = object : Currency {
+ override fun getSymbol() = "ETH"
+ override fun getName() = "Ethereum"
+ override fun getPrecision() = 0.0001
+ }
+
+ private val walletOwner = object : WalletOwner {
+ override fun id() = 2L
+ override fun uuid() = "fdf453d7-0633-4ec7-852d-a18148c99a82"
+ override fun title() = "wallet"
+ override fun level() = "1"
+ override fun isTradeAllowed() = true
+ override fun isWithdrawAllowed() = true
+ override fun isDepositAllowed() = true
+ }
+
+ @Test
+ fun givenWalletWithAllowedTransfer_whenTransfer_thenReturnTransferResultDetailed(): Unit = runBlocking {
+ stubbing(walletOwnerManager) {
+ onBlocking { isWithdrawAllowed(any(), eq(Amount(currency, BigDecimal.valueOf(0.5)))) } doReturn true
+ onBlocking { isDepositAllowed(any(), eq(Amount(currency, BigDecimal.valueOf(0.5)))) } doReturn true
+ }
+ stubbing(walletManager) {
+ onBlocking { isWithdrawAllowed(any(), eq(BigDecimal.valueOf(0.5))) } doReturn true
+ onBlocking { isDepositAllowed(any(), eq(BigDecimal.valueOf(0.5))) } doReturn true
+ onBlocking { decreaseBalance(any(), eq(BigDecimal.valueOf(0.5))) } doReturn Unit
+ onBlocking { increaseBalance(any(), eq(BigDecimal.valueOf(0.5))) } doReturn Unit
+ onBlocking { findWalletById(20L) } doReturn object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(1))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ }
+ stubbing(walletListener) {
+ onBlocking { onWithdraw(any(), any(), any(), anyString(), any()) } doReturn Unit
+ onBlocking { onDeposit(any(), any(), any(), any(), anyString(), any()) } doReturn Unit
+ }
+ stubbing(transactionManager) {
+ onBlocking { save(any()) } doReturn "1"
+ }
+ val sourceWalletOwner = object : WalletOwner {
+ override fun id() = 2L
+ override fun uuid() = "fdf453d7-0633-4ec7-852d-a18148c99a82"
+ override fun title() = "wallet"
+ override fun level() = "1"
+ override fun isTradeAllowed() = true
+ override fun isWithdrawAllowed() = true
+ override fun isDepositAllowed() = true
+ }
+ val sourceWallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = sourceWalletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(1.5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ val destWalletOwner = object : WalletOwner {
+ override fun id() = 3L
+ override fun uuid() = "e1950578-ef22-44e4-89f5-0b78feb03e2a"
+ override fun title() = "wallet"
+ override fun level() = "1"
+ override fun isTradeAllowed() = true
+ override fun isWithdrawAllowed() = true
+ override fun isDepositAllowed() = true
+ }
+ val destWallet = object : Wallet {
+ override fun id() = 30L
+ override fun owner() = destWalletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(2.5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ val transferCommand = TransferCommand(
+ sourceWallet,
+ destWallet,
+ Amount(currency, BigDecimal.valueOf(0.5)),
+ null,
+ null,
+ null
+ )
+
+ val result = transferService.transfer(transferCommand).transferResult
+
+ assertThat(result).isNotNull
+ assertThat(result.amount).isEqualTo(Amount(currency, BigDecimal.valueOf(0.5)))
+ assertThat(result.sourceUuid).isEqualTo("fdf453d7-0633-4ec7-852d-a18148c99a82")
+ assertThat(result.destUuid).isEqualTo("e1950578-ef22-44e4-89f5-0b78feb03e2a")
+ assertThat(result.sourceWalletType).isEqualTo("main")
+ assertThat(result.destWalletType).isEqualTo("main")
+ assertThat(result.sourceBalanceBeforeAction).isEqualTo(Amount(currency, BigDecimal.valueOf(1.5)))
+ assertThat(result.sourceBalanceAfterAction).isEqualTo(Amount(currency, BigDecimal.valueOf(1)))
+ }
+
+ @Test
+ fun givenWalletWithOwnerWithdrawNotAllowed_whenTransfer_thenThrow(): Unit = runBlocking {
+ stubbing(walletOwnerManager) {
+ onBlocking {
+ isWithdrawAllowed(any(), eq(Amount(currency, BigDecimal.valueOf(0.5))))
+ } doReturn false
+ onBlocking { isDepositAllowed(any(), eq(Amount(currency, BigDecimal.valueOf(0.5)))) } doReturn true
+ }
+ stubbing(walletManager) {
+ onBlocking { isWithdrawAllowed(any(), eq(BigDecimal.valueOf(0.5))) } doReturn true
+ onBlocking { isDepositAllowed(any(), eq(BigDecimal.valueOf(0.5))) } doReturn true
+ onBlocking { decreaseBalance(any(), eq(BigDecimal.valueOf(0.5))) } doReturn Unit
+ onBlocking { increaseBalance(any(), eq(BigDecimal.valueOf(0.5))) } doReturn Unit
+ onBlocking { findWalletById(20L) } doReturn object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(1))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ }
+ stubbing(walletListener) {
+ on {
+ runBlocking {
+ onWithdraw(
+ any(),
+ any(),
+ eq(Amount(currency, BigDecimal.valueOf(0.5))),
+ anyString(),
+ any()
+ )
+ }
+ } doReturn Unit
+ on {
+ runBlocking {
+ onDeposit(
+ any(),
+ any(),
+ eq(Amount(currency, BigDecimal.valueOf(0.5))),
+ any(),
+ anyString(),
+ any()
+ )
+ }
+ } doReturn Unit
+ }
+ stubbing(transactionManager) {
+ onBlocking { save(any()) } doReturn "1"
+ }
+ val sourceWalletOwner = object : WalletOwner {
+ override fun id() = 2L
+ override fun uuid() = "fdf453d7-0633-4ec7-852d-a18148c99a82"
+ override fun title() = "wallet"
+ override fun level() = "1"
+ override fun isTradeAllowed() = true
+ override fun isWithdrawAllowed() = true
+ override fun isDepositAllowed() = true
+ }
+ val sourceWallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = sourceWalletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(1.5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ val destWalletOwner = object : WalletOwner {
+ override fun id() = 3L
+ override fun uuid() = "e1950578-ef22-44e4-89f5-0b78feb03e2a"
+ override fun title() = "wallet"
+ override fun level() = "1"
+ override fun isTradeAllowed() = true
+ override fun isWithdrawAllowed() = true
+ override fun isDepositAllowed() = true
+ }
+ val destWallet = object : Wallet {
+ override fun id() = 30L
+ override fun owner() = destWalletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(2.5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ val transferCommand = TransferCommand(
+ sourceWallet,
+ destWallet,
+ Amount(currency, BigDecimal.valueOf(0.5)),
+ null,
+ null,
+ null
+ )
+
+ assertThatThrownBy { runBlocking { transferService.transfer(transferCommand) } }
+ }
+
+ @Test
+ fun givenWalletWithWithdrawNotAllowed_whenTransfer_thenThrow(): Unit = runBlocking {
+ stubbing(walletOwnerManager) {
+ onBlocking { isWithdrawAllowed(any(), eq(Amount(currency, BigDecimal.valueOf(0.5)))) } doReturn true
+ onBlocking { isDepositAllowed(any(), eq(Amount(currency, BigDecimal.valueOf(0.5)))) } doReturn true
+ }
+ stubbing(walletManager) {
+ onBlocking { isWithdrawAllowed(any(), eq(BigDecimal.valueOf(0.5))) } doReturn false
+ onBlocking { isDepositAllowed(any(), eq(BigDecimal.valueOf(0.5))) } doReturn true
+ onBlocking { decreaseBalance(any(), eq(BigDecimal.valueOf(0.5))) } doReturn Unit
+ onBlocking { increaseBalance(any(), eq(BigDecimal.valueOf(0.5))) } doReturn Unit
+ onBlocking { findWalletById(20L) } doReturn object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(1))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ }
+ stubbing(walletListener) {
+ on {
+ runBlocking {
+ onWithdraw(
+ any(),
+ any(),
+ eq(Amount(currency, BigDecimal.valueOf(0.5))),
+ anyString(),
+ any()
+ )
+ }
+ } doReturn Unit
+ on {
+ runBlocking {
+ onDeposit(
+ any(),
+ any(),
+ eq(Amount(currency, BigDecimal.valueOf(0.5))),
+ any(),
+ anyString(),
+ any()
+ )
+ }
+ } doReturn Unit
+ }
+ stubbing(transactionManager) {
+ onBlocking { save(any()) } doReturn "1"
+ }
+ val sourceWalletOwner = object : WalletOwner {
+ override fun id() = 2L
+ override fun uuid() = "fdf453d7-0633-4ec7-852d-a18148c99a82"
+ override fun title() = "wallet"
+ override fun level() = "1"
+ override fun isTradeAllowed() = true
+ override fun isWithdrawAllowed() = true
+ override fun isDepositAllowed() = true
+ }
+ val sourceWallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = sourceWalletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(1.5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ val destWalletOwner = object : WalletOwner {
+ override fun id() = 3L
+ override fun uuid() = "e1950578-ef22-44e4-89f5-0b78feb03e2a"
+ override fun title() = "wallet"
+ override fun level() = "1"
+ override fun isTradeAllowed() = true
+ override fun isWithdrawAllowed() = true
+ override fun isDepositAllowed() = true
+ }
+ val destWallet = object : Wallet {
+ override fun id() = 30L
+ override fun owner() = destWalletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(2.5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ val transferCommand = TransferCommand(
+ sourceWallet,
+ destWallet,
+ Amount(currency, BigDecimal.valueOf(0.5)),
+ null,
+ null,
+ null
+ )
+
+ assertThatThrownBy { runBlocking { transferService.transfer(transferCommand) } }
+ }
+
+ @Test
+ fun givenWalletWithOwnerDepositNotAllowed_whenTransfer_thenThrow(): Unit = runBlocking {
+ stubbing(walletOwnerManager) {
+ onBlocking { isWithdrawAllowed(any(), eq(Amount(currency, BigDecimal.valueOf(0.5)))) } doReturn true
+ onBlocking { isDepositAllowed(any(), eq(Amount(currency, BigDecimal.valueOf(0.5)))) } doReturn false
+ }
+ stubbing(walletManager) {
+ onBlocking { isWithdrawAllowed(any(), eq(BigDecimal.valueOf(0.5))) } doReturn true
+ onBlocking { isDepositAllowed(any(), eq(BigDecimal.valueOf(0.5))) } doReturn true
+ onBlocking { decreaseBalance(any(), eq(BigDecimal.valueOf(0.5))) } doReturn Unit
+ onBlocking { increaseBalance(any(), eq(BigDecimal.valueOf(0.5))) } doReturn Unit
+ onBlocking { findWalletById(20L) } doReturn object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(1))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ }
+ stubbing(walletListener) {
+ on {
+ runBlocking {
+ onWithdraw(
+ any(),
+ any(),
+ eq(Amount(currency, BigDecimal.valueOf(0.5))),
+ anyString(),
+ any()
+ )
+ }
+ } doReturn Unit
+ on {
+ runBlocking {
+ onDeposit(
+ any(),
+ any(),
+ eq(Amount(currency, BigDecimal.valueOf(0.5))),
+ any(),
+ anyString(),
+ any()
+ )
+ }
+ } doReturn Unit
+ }
+ stubbing(transactionManager) {
+ onBlocking { save(any()) } doReturn "1"
+ }
+ val sourceWalletOwner = object : WalletOwner {
+ override fun id() = 2L
+ override fun uuid() = "fdf453d7-0633-4ec7-852d-a18148c99a82"
+ override fun title() = "wallet"
+ override fun level() = "1"
+ override fun isTradeAllowed() = true
+ override fun isWithdrawAllowed() = true
+ override fun isDepositAllowed() = true
+ }
+ val sourceWallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = sourceWalletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(1.5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ val destWalletOwner = object : WalletOwner {
+ override fun id() = 3L
+ override fun uuid() = "e1950578-ef22-44e4-89f5-0b78feb03e2a"
+ override fun title() = "wallet"
+ override fun level() = "1"
+ override fun isTradeAllowed() = true
+ override fun isWithdrawAllowed() = true
+ override fun isDepositAllowed() = true
+ }
+ val destWallet = object : Wallet {
+ override fun id() = 30L
+ override fun owner() = destWalletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(2.5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ val transferCommand = TransferCommand(
+ sourceWallet,
+ destWallet,
+ Amount(currency, BigDecimal.valueOf(0.5)),
+ null,
+ null,
+ null
+ )
+
+ assertThatThrownBy { runBlocking { transferService.transfer(transferCommand) } }
+ }
+
+ @Test
+ fun givenWalletWithDepositNotAllowed_whenTransfer_thenThrow(): Unit = runBlocking {
+ stubbing(walletOwnerManager) {
+ onBlocking { isWithdrawAllowed(any(), eq(Amount(currency, BigDecimal.valueOf(0.5)))) } doReturn true
+ onBlocking { isDepositAllowed(any(), eq(Amount(currency, BigDecimal.valueOf(0.5)))) } doReturn true
+ }
+ stubbing(walletManager) {
+ onBlocking { isWithdrawAllowed(any(), eq(BigDecimal.valueOf(0.5))) } doReturn true
+ onBlocking { isDepositAllowed(any(), eq(BigDecimal.valueOf(0.5))) } doReturn false
+ onBlocking { decreaseBalance(any(), eq(BigDecimal.valueOf(0.5))) } doReturn Unit
+ onBlocking { increaseBalance(any(), eq(BigDecimal.valueOf(0.5))) } doReturn Unit
+ onBlocking { findWalletById(20L) } doReturn object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(1))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ }
+ stubbing(walletListener) {
+ on {
+ runBlocking {
+ onWithdraw(
+ any(),
+ any(),
+ eq(Amount(currency, BigDecimal.valueOf(0.5))),
+ anyString(),
+ any()
+ )
+ }
+ } doReturn Unit
+ on {
+ runBlocking {
+ onDeposit(
+ any(),
+ any(),
+ eq(Amount(currency, BigDecimal.valueOf(0.5))),
+ any(),
+ anyString(),
+ any()
+ )
+ }
+ } doReturn Unit
+ }
+ stubbing(transactionManager) {
+ onBlocking { save(any()) } doReturn "1"
+ }
+ val sourceWalletOwner = object : WalletOwner {
+ override fun id() = 2L
+ override fun uuid() = "fdf453d7-0633-4ec7-852d-a18148c99a82"
+ override fun title() = "wallet"
+ override fun level() = "1"
+ override fun isTradeAllowed() = true
+ override fun isWithdrawAllowed() = true
+ override fun isDepositAllowed() = true
+ }
+ val sourceWallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = sourceWalletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(1.5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ val destWalletOwner = object : WalletOwner {
+ override fun id() = 3L
+ override fun uuid() = "e1950578-ef22-44e4-89f5-0b78feb03e2a"
+ override fun title() = "wallet"
+ override fun level() = "1"
+ override fun isTradeAllowed() = true
+ override fun isWithdrawAllowed() = true
+ override fun isDepositAllowed() = true
+ }
+ val destWallet = object : Wallet {
+ override fun id() = 30L
+ override fun owner() = destWalletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(2.5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ val transferCommand = TransferCommand(
+ sourceWallet,
+ destWallet,
+ Amount(currency, BigDecimal.valueOf(0.5)),
+ null,
+ null,
+ null
+ )
+
+ assertThatThrownBy { runBlocking { transferService.transfer(transferCommand) } }
+ }
+
+ @Test
+ fun givenNoWallet_whenTransfer_thenThrow(): Unit = runBlocking {
+ stubbing(walletOwnerManager) {
+ onBlocking { isWithdrawAllowed(any(), eq(Amount(currency, BigDecimal.valueOf(0.5)))) } doReturn true
+ onBlocking { isDepositAllowed(any(), eq(Amount(currency, BigDecimal.valueOf(0.5)))) } doReturn true
+ }
+ stubbing(walletManager) {
+ onBlocking { isWithdrawAllowed(any(), eq(BigDecimal.valueOf(0.5))) } doReturn true
+ onBlocking { isDepositAllowed(any(), eq(BigDecimal.valueOf(0.5))) } doReturn true
+ onBlocking { decreaseBalance(any(), eq(BigDecimal.valueOf(0.5))) } doReturn Unit
+ onBlocking { increaseBalance(any(), eq(BigDecimal.valueOf(0.5))) } doReturn Unit
+ onBlocking { findWalletById(20L) } doReturn null
+ }
+ stubbing(walletListener) {
+ onBlocking {
+ onWithdraw(
+ any(),
+ any(),
+ eq(Amount(currency, BigDecimal.valueOf(0.5))),
+ anyString(),
+ any()
+ )
+ } doReturn Unit
+ on {
+ runBlocking {
+ onDeposit(
+ any(),
+ any(),
+ eq(Amount(currency, BigDecimal.valueOf(0.5))),
+ any(),
+ anyString(),
+ any()
+ )
+ }
+ } doReturn Unit
+ }
+ stubbing(transactionManager) {
+ onBlocking { save(any()) } doReturn "1"
+ }
+ val sourceWalletOwner = object : WalletOwner {
+ override fun id() = 2L
+ override fun uuid() = "fdf453d7-0633-4ec7-852d-a18148c99a82"
+ override fun title() = "wallet"
+ override fun level() = "1"
+ override fun isTradeAllowed() = true
+ override fun isWithdrawAllowed() = true
+ override fun isDepositAllowed() = true
+ }
+ val sourceWallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = sourceWalletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(1.5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ val destWalletOwner = object : WalletOwner {
+ override fun id() = 3L
+ override fun uuid() = "e1950578-ef22-44e4-89f5-0b78feb03e2a"
+ override fun title() = "wallet"
+ override fun level() = "1"
+ override fun isTradeAllowed() = true
+ override fun isWithdrawAllowed() = true
+ override fun isDepositAllowed() = true
+ }
+ val destWallet = object : Wallet {
+ override fun id() = 30L
+ override fun owner() = destWalletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(2.5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ val transferCommand = TransferCommand(
+ sourceWallet,
+ destWallet,
+ Amount(currency, BigDecimal.valueOf(0.5)),
+ null,
+ null,
+ null
+ )
+
+ assertThatThrownBy { runBlocking { transferService.transfer(transferCommand) } }
+ }
+}
diff --git a/wallet/wallet-ports/wallet-persister-postgres/pom.xml b/wallet/wallet-ports/wallet-persister-postgres/pom.xml
index aef5f1549..84d618043 100644
--- a/wallet/wallet-ports/wallet-persister-postgres/pom.xml
+++ b/wallet/wallet-ports/wallet-persister-postgres/pom.xml
@@ -60,5 +60,9 @@
reactor-test
test
+
+ org.mockito.kotlin
+ mockito-kotlin
+
diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/test/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyServiceTest.kt b/wallet/wallet-ports/wallet-persister-postgres/src/test/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyServiceTest.kt
new file mode 100644
index 000000000..fc6701881
--- /dev/null
+++ b/wallet/wallet-ports/wallet-persister-postgres/src/test/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyServiceTest.kt
@@ -0,0 +1,49 @@
+package co.nilin.opex.wallet.ports.postgres.impl
+
+import co.nilin.opex.wallet.ports.postgres.dao.CurrencyRepository
+import co.nilin.opex.wallet.ports.postgres.model.CurrencyModel
+import kotlinx.coroutines.runBlocking
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stubbing
+import reactor.core.publisher.Mono
+
+private class CurrencyServiceTest {
+ private val currencyRepository: CurrencyRepository = mock { }
+ private val currencyService: CurrencyServiceImpl = CurrencyServiceImpl(currencyRepository)
+
+ @Test
+ fun givenCurrency_whenGetCurrency_thenReturnCurrency(): Unit = runBlocking {
+ stubbing(currencyRepository) {
+ on { findBySymbol("ETH") } doReturn Mono.just(CurrencyModel("ETH", "Ethereum", 0.0001))
+ }
+ val c = currencyService.getCurrency("ETH")
+
+ assertThat(c).isNotNull
+ assertThat(c!!.getSymbol()).isEqualTo("ETH")
+ assertThat(c.getName()).isEqualTo("Ethereum")
+ assertThat(c.getPrecision()).isEqualTo(0.0001)
+ }
+
+ @Test
+ fun givenNoCurrency_whenGetCurrency_thenReturnNull(): Unit = runBlocking {
+ stubbing(currencyRepository) {
+ on { findBySymbol("ETH") } doReturn Mono.empty()
+ }
+ val c = currencyService.getCurrency("ETH")
+
+ assertThat(c).isNull()
+ }
+
+ @Test
+ fun givenNoCurrency_whenGetCurrencyWithEmptySymbol_thenReturnNull(): Unit = runBlocking {
+ stubbing(currencyRepository) {
+ on { findBySymbol("") } doReturn Mono.empty()
+ }
+ val c = currencyService.getCurrency("")
+
+ assertThat(c).isNull()
+ }
+}
diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/test/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletManagerTest.kt b/wallet/wallet-ports/wallet-persister-postgres/src/test/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletManagerTest.kt
new file mode 100644
index 000000000..b799cc851
--- /dev/null
+++ b/wallet/wallet-ports/wallet-persister-postgres/src/test/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletManagerTest.kt
@@ -0,0 +1,1010 @@
+package co.nilin.opex.wallet.ports.postgres.impl
+
+import co.nilin.opex.wallet.core.model.Amount
+import co.nilin.opex.wallet.core.model.Currency
+import co.nilin.opex.wallet.core.model.Wallet
+import co.nilin.opex.wallet.core.model.WalletOwner
+import co.nilin.opex.wallet.ports.postgres.dao.*
+import co.nilin.opex.wallet.ports.postgres.model.CurrencyModel
+import co.nilin.opex.wallet.ports.postgres.model.WalletLimitsModel
+import co.nilin.opex.wallet.ports.postgres.model.WalletModel
+import co.nilin.opex.wallet.ports.postgres.model.WalletOwnerModel
+import kotlinx.coroutines.runBlocking
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.junit.jupiter.api.Test
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.kotlin.*
+import reactor.core.publisher.Mono
+import java.math.BigDecimal
+
+private class WalletManagerTest {
+ private val walletLimitsRepository: WalletLimitsRepository = mock()
+ private val walletRepository: WalletRepository = mock()
+ private val walletOwnerRepository: WalletOwnerRepository = mock()
+ private val currencyRepository: CurrencyRepository = mock { }
+
+ private var transactionRepository: TransactionRepository = mock {
+ on { calculateWithdrawStatistics(eq(2), eq(20), any(), any()) } doReturn Mono.empty()
+ }
+
+ private val walletManagerImpl: WalletManagerImpl = WalletManagerImpl(
+ walletLimitsRepository, transactionRepository, walletRepository, walletOwnerRepository, currencyRepository
+ )
+
+ private val walletOwner = object : WalletOwner {
+ override fun id() = 2L
+ override fun uuid() = "fdf453d7-0633-4ec7-852d-a18148c99a82"
+ override fun title() = "wallet"
+ override fun level() = "1"
+ override fun isTradeAllowed() = true
+ override fun isWithdrawAllowed() = true
+ override fun isDepositAllowed() = true
+ }
+
+ private val currency = object : Currency {
+ override fun getSymbol() = "ETH"
+ override fun getName() = "Ethereum"
+ override fun getPrecision() = 0.0001
+ }
+
+ @Test
+ fun givenWalletWithNoLimit_whenIsWithdrawAllowed_thenReturnTrue(): Unit = runBlocking {
+ stubbing(walletLimitsRepository) {
+ on {
+ findByOwnerAndCurrencyAndWalletAndAction(walletOwner.id(), "ETH", 20, "withdraw")
+ } doReturn Mono.empty()
+ on {
+ findByOwnerAndCurrencyAndActionAndWalletType(walletOwner.id(), "ETH", "withdraw", "main")
+ } doReturn Mono.empty()
+ on {
+ findByLevelAndCurrencyAndActionAndWalletType("1", "ETH", "withdraw", "main")
+ } doReturn Mono.empty()
+ }
+ val wallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(0.5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ val isAllowed = walletManagerImpl.isWithdrawAllowed(wallet, BigDecimal.valueOf(0.5))
+
+ assertThat(isAllowed).isTrue()
+ }
+
+ @Test
+ fun givenNoWallet_whenIsWithdrawAllowed_thenThrow(): Unit = runBlocking {
+ val wallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(0))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ stubbing(walletLimitsRepository) {
+ on {
+ findByOwnerAndCurrencyAndWalletAndAction(anyLong(), "ETH", anyLong(), "withdraw")
+ } doReturn Mono.empty()
+ on {
+ findByOwnerAndCurrencyAndActionAndWalletType(anyLong(), "ETH", "withdraw", "main")
+ } doReturn Mono.empty()
+ on {
+ findByLevelAndCurrencyAndActionAndWalletType("1", "ETH", "withdraw", "main")
+ } doReturn Mono.empty()
+ }
+
+ assertThatThrownBy {
+ runBlocking {
+ walletManagerImpl.isWithdrawAllowed(
+ wallet,
+ BigDecimal.valueOf(0.5)
+ )
+ }
+ }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun giNoCurrency_whenIsWithdrawAllowed_thenThrow(): Unit = runBlocking {
+ stubbing(currencyRepository) {
+ on { findBySymbol(currency.getSymbol()) } doReturn Mono.empty()
+ on { findById(currency.getSymbol()) } doReturn Mono.empty()
+ }
+ val wallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(0))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ assertThatThrownBy {
+ runBlocking {
+ walletManagerImpl.isWithdrawAllowed(
+ wallet,
+ BigDecimal.valueOf(0.5)
+ )
+ }
+ }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenEmptyWallet_whenIsWithdrawAllowed_thenReturnFalse(): Unit = runBlocking {
+ val wallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(0))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ val isAllowed = walletManagerImpl.isWithdrawAllowed(wallet, BigDecimal.valueOf(0.5))
+
+ assertThat(isAllowed).isFalse()
+ }
+
+ @Test
+ fun givenWrongAmount_whenIsWithdrawAllowed_thenThrow(): Unit = runBlocking {
+ val wallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(0))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ assertThatThrownBy {
+ runBlocking {
+ walletManagerImpl.isWithdrawAllowed(
+ wallet,
+ BigDecimal.valueOf(-1)
+ )
+ }
+ }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenOwnerAndWalletTypeLimit_whenIsWithdrawAllowed_thenReturnTrue(): Unit = runBlocking {
+ stubbing(walletLimitsRepository) {
+ on {
+ findByOwnerAndCurrencyAndWalletAndAction(2, "ETH", 30, "withdraw")
+ } doReturn Mono.empty()
+ on {
+ findByOwnerAndCurrencyAndActionAndWalletType(2, "ETH", "withdraw", "main")
+ } doReturn Mono.just(
+ WalletLimitsModel(
+ 1,
+ null,
+ 2,
+ "withdraw",
+ "ETH",
+ "main",
+ 30,
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ on {
+ findByLevelAndCurrencyAndActionAndWalletType("1", "ETH", "withdraw", "main")
+ } doReturn Mono.empty()
+ }
+ val wallet = object : Wallet {
+ override fun id() = 30L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ val isAllowed = walletManagerImpl.isWithdrawAllowed(wallet, BigDecimal.valueOf(1))
+
+ assertThat(isAllowed).isTrue()
+ }
+
+ @Test
+ fun givenOwnerAndWalletLimit_whenIsWithdrawAllowed_thenReturnTrue(): Unit = runBlocking {
+ stubbing(walletLimitsRepository) {
+ on {
+ findByOwnerAndCurrencyAndWalletAndAction(2, "ETH", 30, "withdraw")
+ } doReturn Mono.just(
+ WalletLimitsModel(
+ 1,
+ null,
+ 2,
+ "withdraw",
+ "ETH",
+ "main",
+ 30,
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ on {
+ findByOwnerAndCurrencyAndActionAndWalletType(2, "ETH", "withdraw", "main")
+ } doReturn Mono.empty()
+ on {
+ findByLevelAndCurrencyAndActionAndWalletType("1", "ETH", "withdraw", "main")
+ } doReturn Mono.empty()
+ }
+ val wallet = object : Wallet {
+ override fun id() = 30L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ val isAllowed = walletManagerImpl.isWithdrawAllowed(wallet, BigDecimal.valueOf(1))
+
+ assertThat(isAllowed).isTrue()
+ }
+
+ @Test
+ fun givenLevelAndWalletTypeLimit_whenIsWithdrawAllowed_thenReturnTrue(): Unit = runBlocking {
+ stubbing(walletLimitsRepository) {
+ on {
+ findByOwnerAndCurrencyAndWalletAndAction(2, "ETH", 30, "withdraw")
+ } doReturn Mono.empty()
+ on {
+ findByOwnerAndCurrencyAndActionAndWalletType(2, "ETH", "withdraw", "main")
+ } doReturn Mono.empty()
+ on {
+ findByLevelAndCurrencyAndActionAndWalletType("1", "ETH", "withdraw", "main")
+ } doReturn Mono.just(
+ WalletLimitsModel(
+ 1,
+ "1",
+ 2,
+ "withdraw",
+ "ETH",
+ "main",
+ 30,
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ }
+ val wallet = object : Wallet {
+ override fun id() = 30L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ val isAllowed = walletManagerImpl.isWithdrawAllowed(wallet, BigDecimal.valueOf(1))
+
+ assertThat(isAllowed).isTrue()
+ }
+
+ @Test
+ fun givenAllLimits_whenIsWithdrawAllowedWithValidAmount_thenReturnTrue(): Unit = runBlocking {
+ stubbing(walletLimitsRepository) {
+ on {
+ findByOwnerAndCurrencyAndWalletAndAction(2, "ETH", 30, "withdraw")
+ } doReturn Mono.just(
+ WalletLimitsModel(
+ 1,
+ null,
+ 2,
+ "withdraw",
+ "ETH",
+ "main",
+ 30,
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ on {
+ findByOwnerAndCurrencyAndActionAndWalletType(2, "ETH", "withdraw", "main")
+ } doReturn Mono.just(
+ WalletLimitsModel(
+ 1,
+ null,
+ 2,
+ "withdraw",
+ "ETH",
+ "main",
+ 30,
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ on {
+ findByLevelAndCurrencyAndActionAndWalletType("1", "ETH", "withdraw", "main")
+ } doReturn Mono.just(
+ WalletLimitsModel(
+ 1,
+ "1",
+ 2,
+ "withdraw",
+ "ETH",
+ "main",
+ 30,
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ }
+ val wallet = object : Wallet {
+ override fun id() = 30L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ val isAllowed = walletManagerImpl.isWithdrawAllowed(wallet, BigDecimal.valueOf(1))
+
+ assertThat(isAllowed).isTrue()
+ }
+
+ @Test
+ fun givenAllLimits_whenIsWithdrawAllowedWithInvalidAmount_thenReturnFalse(): Unit = runBlocking {
+ stubbing(walletLimitsRepository) {
+ on {
+ findByOwnerAndCurrencyAndWalletAndAction(2, "ETH", 30, "withdraw")
+ } doReturn Mono.just(
+ WalletLimitsModel(
+ 1,
+ null,
+ 2,
+ "withdraw",
+ "ETH",
+ "main",
+ 30,
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ on {
+ findByOwnerAndCurrencyAndActionAndWalletType(2, "ETH", "withdraw", "main")
+ } doReturn Mono.just(
+ WalletLimitsModel(
+ 1,
+ null,
+ 2,
+ "withdraw",
+ "ETH",
+ "main",
+ 30,
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ on {
+ findByLevelAndCurrencyAndActionAndWalletType("1", "ETH", "withdraw", "main")
+ } doReturn Mono.just(
+ WalletLimitsModel(
+ 1,
+ "1",
+ 2,
+ "withdraw",
+ "ETH",
+ "main",
+ 30,
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ }
+ val wallet = object : Wallet {
+ override fun id() = 30L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(500))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ val isAllowed = walletManagerImpl.isWithdrawAllowed(wallet, BigDecimal.valueOf(30))
+
+ assertThat(isAllowed).isFalse()
+ }
+
+ @Test
+ fun givenEmptyWalletWithNoLimit_whenIsWithdrawAllowed_thenReturnFalse(): Unit = runBlocking {
+ stubbing(walletLimitsRepository) {
+ on {
+ findByOwnerAndCurrencyAndWalletAndAction(walletOwner.id(), "ETH", 20, "withdraw")
+ } doReturn Mono.empty()
+ on {
+ findByOwnerAndCurrencyAndActionAndWalletType(walletOwner.id(), "ETH", "withdraw", "main")
+ } doReturn Mono.empty()
+ on {
+ findByLevelAndCurrencyAndActionAndWalletType("1", "ETH", "withdraw", "main")
+ } doReturn Mono.empty()
+ }
+ val wallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(0))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ val isAllowed = walletManagerImpl.isWithdrawAllowed(wallet, BigDecimal.valueOf(0.5))
+
+ assertThat(isAllowed).isFalse()
+ }
+
+ @Test
+ fun givenWalletWithNoLimit_whenIsDepositAllowed_thenReturnTrue(): Unit = runBlocking {
+ stubbing(walletLimitsRepository) {
+ on {
+ findByOwnerAndCurrencyAndWalletAndAction(walletOwner.id(), "ETH", 20, "deposit")
+ } doReturn Mono.empty()
+ on {
+ findByOwnerAndCurrencyAndActionAndWalletType(walletOwner.id(), "ETH", "deposit", "main")
+ } doReturn Mono.empty()
+ on {
+ findByLevelAndCurrencyAndActionAndWalletType("1", "ETH", "deposit", "main")
+ } doReturn Mono.empty()
+ }
+ val wallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(0.5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ val isAllowed = walletManagerImpl.isDepositAllowed(wallet, BigDecimal.valueOf(0.5))
+
+ assertThat(isAllowed).isTrue()
+ }
+
+ @Test
+ fun givenNotExistWallet_whenIsDepositAllowed_thenThrow(): Unit = runBlocking {
+ val wallet = object : Wallet {
+ override fun id() = 40L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(0))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ assertThatThrownBy {
+ runBlocking {
+ walletManagerImpl.isDepositAllowed(
+ wallet,
+ BigDecimal.valueOf(0.5)
+ )
+ }
+ }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenNoCurrency_whenIsDepositAllowed_thenThrow(): Unit = runBlocking {
+ val wallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(0))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+ stubbing(currencyRepository) {
+ on { findBySymbol(anyString()) } doReturn Mono.empty()
+ on { findById(anyString()) } doReturn Mono.empty()
+ }
+
+ assertThatThrownBy {
+ runBlocking {
+ walletManagerImpl.isDepositAllowed(
+ wallet,
+ BigDecimal.valueOf(0.5)
+ )
+ }
+ }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenEmptyWallet_whenIsDepositAllowed_thenFalse(): Unit = runBlocking {
+ stubbing(walletLimitsRepository) {
+ on {
+ findByOwnerAndCurrencyAndWalletAndAction(walletOwner.id(), "ETH", 20, "withdraw")
+ } doReturn Mono.empty()
+ on {
+ findByOwnerAndCurrencyAndActionAndWalletType(walletOwner.id(), "ETH", "withdraw", "main")
+ } doReturn Mono.empty()
+ on {
+ findByLevelAndCurrencyAndActionAndWalletType("1", "ETH", "withdraw", "main")
+ } doReturn Mono.empty()
+ }
+ val wallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(0))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ val isAllowed = runBlocking { walletManagerImpl.isDepositAllowed(wallet, BigDecimal.valueOf(0.5)) }
+
+ verify(walletLimitsRepository, never()).findByOwnerAndCurrencyAndWalletAndAction(
+ walletOwner.id(),
+ "ETH",
+ 20,
+ "withdraw"
+ )
+ verify(walletLimitsRepository, never()).findByOwnerAndCurrencyAndActionAndWalletType(
+ walletOwner.id(),
+ "ETH",
+ "withdraw",
+ "main"
+ )
+ verify(walletLimitsRepository, never()).findByLevelAndCurrencyAndActionAndWalletType(
+ "1",
+ "ETH",
+ "withdraw",
+ "main"
+ )
+ assertThat(isAllowed).isFalse()
+ }
+
+ @Test
+ fun givenWrongAmount_whenIsDepositAllowed_thenThrow(): Unit = runBlocking {
+ val wallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(0))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ assertThatThrownBy { runBlocking { walletManagerImpl.isDepositAllowed(wallet, BigDecimal.valueOf(-1)) } }
+ }
+
+ @Test
+ fun givenAllLimits_whenIsDepositAllowedWithValidAmount_thenReturnTrue(): Unit = runBlocking {
+ stubbing(walletLimitsRepository) {
+ on {
+ findByOwnerAndCurrencyAndWalletAndAction(2, "ETH", 30, "deposit")
+ } doReturn Mono.just(
+ WalletLimitsModel(
+ 1,
+ null,
+ 2,
+ "deposit",
+ "ETH",
+ "main",
+ 30,
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ on {
+ findByOwnerAndCurrencyAndActionAndWalletType(2, "ETH", "deposit", "main")
+ } doReturn Mono.just(
+ WalletLimitsModel(
+ 1,
+ null,
+ 2,
+ "deposit",
+ "ETH",
+ "main",
+ 30,
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ on {
+ findByLevelAndCurrencyAndActionAndWalletType("1", "ETH", "deposit", "main")
+ } doReturn Mono.just(
+ WalletLimitsModel(
+ 1,
+ "1",
+ 2,
+ "deposit",
+ "ETH",
+ "main",
+ 30,
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ }
+ val wallet = object : Wallet {
+ override fun id() = 30L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(5))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ val isAllowed = walletManagerImpl.isDepositAllowed(wallet, BigDecimal.valueOf(1))
+
+ assertThat(isAllowed).isTrue()
+ }
+
+ @Test
+ fun givenWalletWithWalletLimit_whenIsDepositAllowed_thenReturnFalse(): Unit = runBlocking {
+ stubbing(walletLimitsRepository) {
+ on {
+ findByOwnerAndCurrencyAndWalletAndAction(2, "ETH", 30, "deposit")
+ } doReturn Mono.just(
+ WalletLimitsModel(
+ 1,
+ null,
+ 2,
+ "deposit",
+ "ETH",
+ "main",
+ 30,
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ on {
+ findByOwnerAndCurrencyAndActionAndWalletType(2, "ETH", "deposit", "main")
+ } doReturn Mono.just(
+ WalletLimitsModel(
+ 1,
+ null,
+ 2,
+ "deposit",
+ "ETH",
+ "main",
+ 30,
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ on {
+ findByLevelAndCurrencyAndActionAndWalletType("1", "ETH", "deposit", "main")
+ } doReturn Mono.just(
+ WalletLimitsModel(
+ 1,
+ "1",
+ 2,
+ "deposit",
+ "ETH",
+ "main",
+ 30,
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ }
+ val wallet = object : Wallet {
+ override fun id() = 30L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(500))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ val isAllowed = walletManagerImpl.isDepositAllowed(wallet, BigDecimal.valueOf(30))
+
+ assertThat(isAllowed).isFalse()
+ }
+
+ @Test
+ fun givenEmptyWalletWithNoLimit_whenIsDepositAllowed_thenReturnFalse(): Unit = runBlocking {
+ stubbing(walletLimitsRepository) {
+ on {
+ findByOwnerAndCurrencyAndWalletAndAction(walletOwner.id(), "ETH", 20, "deposit")
+ } doReturn Mono.empty()
+ on {
+ findByOwnerAndCurrencyAndActionAndWalletType(walletOwner.id(), "ETH", "deposit", "main")
+ } doReturn Mono.empty()
+ on {
+ findByLevelAndCurrencyAndActionAndWalletType("1", "ETH", "deposit", "main")
+ } doReturn Mono.empty()
+ }
+ val wallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(0))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ val isAllowed = walletManagerImpl.isDepositAllowed(wallet, BigDecimal.valueOf(0.5))
+
+ assertThat(isAllowed).isFalse()
+ }
+
+ @Test
+ fun givenWallet_whenFindWalletByOwnerAndCurrencyAndType_thenReturnWallet(): Unit = runBlocking {
+ stubbing(walletOwnerRepository) {
+ on { findById(walletOwner.id()) } doReturn Mono.just(
+ WalletOwnerModel(
+ walletOwner.id(),
+ walletOwner.uuid(),
+ walletOwner.title(),
+ walletOwner.level(),
+ walletOwner.isTradeAllowed(),
+ walletOwner.isWithdrawAllowed(),
+ walletOwner.isDepositAllowed()
+ )
+ )
+ }
+ stubbing(walletRepository) {
+ on {
+ findByOwnerAndTypeAndCurrency(walletOwner.id(), "main", currency.getSymbol())
+ } doReturn Mono.just(
+ WalletModel(
+ 20L,
+ walletOwner.id(),
+ "main",
+ currency.getSymbol(),
+ BigDecimal.valueOf(1.2)
+ )
+ )
+ }
+ stubbing(currencyRepository) {
+ on {
+ findBySymbol(currency.getSymbol())
+ } doReturn Mono.just(
+ CurrencyModel(
+ currency.getSymbol(),
+ currency.getName(),
+ currency.getPrecision()
+ )
+ )
+ }
+
+ val wallet = walletManagerImpl.findWalletByOwnerAndCurrencyAndType(walletOwner, "main", currency)
+
+ assertThat(wallet).isNotNull
+ assertThat(wallet!!.owner().id()).isEqualTo(walletOwner.id())
+ assertThat(wallet.currency().getSymbol()).isEqualTo(currency.getSymbol())
+ assertThat(wallet.type()).isEqualTo("main")
+ }
+
+ @Test
+ fun givenEmptyWalletWithNoLimit_whenCreateWallet_thenReturnWallet(): Unit = runBlocking {
+ stubbing(walletRepository) {
+ on {
+ save(WalletModel(null, walletOwner.id(), "main", currency.getSymbol(), BigDecimal.valueOf(1)))
+ } doReturn Mono.just(
+ WalletModel(
+ 20L,
+ walletOwner.id(),
+ "main",
+ currency.getSymbol(),
+ BigDecimal.valueOf(1)
+ )
+ )
+ }
+
+ val wallet = walletManagerImpl.createWallet(
+ walletOwner,
+ Amount(currency, BigDecimal.valueOf(1)),
+ currency,
+ "main"
+ )
+
+ assertThat(wallet).isNotNull
+ assertThat(wallet.owner().id()).isEqualTo(walletOwner.id())
+ assertThat(wallet.currency().getSymbol()).isEqualTo(currency.getSymbol())
+ assertThat(wallet.type()).isEqualTo("main")
+ }
+
+ @Test
+ fun givenWallet_whenIncreaseBalance_thenSuccess(): Unit = runBlocking {
+ stubbing(walletRepository) {
+ on {
+ updateBalance(eq(20), any())
+ } doReturn Mono.just(1)
+ }
+ val wallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(2))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ assertThatThrownBy {
+ runBlocking {
+ walletManagerImpl.increaseBalance(
+ wallet,
+ BigDecimal.valueOf(1)
+ )
+ }
+ }.doesNotThrowAnyException()
+ }
+
+ @Test
+ fun givenNoWallet_whenIncreaseBalance_thenThrow(): Unit = runBlocking {
+ stubbing(walletRepository) {
+ on {
+ updateBalance(any(), eq(BigDecimal.valueOf(1)))
+ } doReturn Mono.just(0)
+ }
+ val wallet = object : Wallet {
+ override fun id() = 40L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(2))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ assertThatThrownBy {
+ runBlocking {
+ walletManagerImpl.increaseBalance(
+ wallet,
+ BigDecimal.valueOf(1)
+ )
+ }
+ }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenWrongAmount_whenIncreaseBalance_thenThrow(): Unit = runBlocking {
+ stubbing(walletRepository) {
+ on {
+ updateBalance(eq(20), any())
+ } doReturn Mono.just(0)
+ }
+ val wallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(2))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ assertThatThrownBy {
+ runBlocking {
+ walletManagerImpl.increaseBalance(
+ wallet,
+ BigDecimal.valueOf(-1)
+ )
+ }
+ }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenWallet_whenDecreaseBalance_thenSuccess(): Unit = runBlocking {
+ stubbing(walletRepository) {
+ on { updateBalance(eq(20), eq(BigDecimal.valueOf(-1))) } doReturn Mono.just(1)
+ }
+ val wallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(2))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ assertThatThrownBy {
+ runBlocking {
+ walletManagerImpl.decreaseBalance(
+ wallet,
+ BigDecimal.valueOf(1)
+ )
+ }
+ }.doesNotThrowAnyException()
+ }
+
+ @Test
+ fun givenNoWallet_whenDecreaseBalance_thenThrow(): Unit = runBlocking {
+ stubbing(walletRepository) {
+ on {
+ updateBalance(any(), eq(BigDecimal.valueOf(-1)))
+ } doReturn Mono.just(0)
+ }
+ val wallet = object : Wallet {
+ override fun id() = 40L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(2))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ assertThatThrownBy {
+ runBlocking {
+ walletManagerImpl.decreaseBalance(
+ wallet,
+ BigDecimal.valueOf(1)
+ )
+ }
+ }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenWrongAmount_whenDecreaseBalance_thenThrow(): Unit = runBlocking {
+ stubbing(walletRepository) {
+ on {
+ updateBalance(eq(20), eq(BigDecimal.valueOf(-1)))
+ } doReturn Mono.just(0)
+ }
+ val wallet = object : Wallet {
+ override fun id() = 20L
+ override fun owner() = walletOwner
+ override fun balance() = Amount(currency, BigDecimal.valueOf(2))
+ override fun currency() = currency
+ override fun type() = "main"
+ }
+
+ assertThatThrownBy {
+ runBlocking {
+ walletManagerImpl.decreaseBalance(
+ wallet,
+ BigDecimal.valueOf(-1)
+ )
+ }
+ }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenWallet_whenFindWalletById_thenReturnWallet(): Unit = runBlocking {
+ stubbing(walletRepository) {
+ on { findById(20) } doReturn Mono.just(
+ WalletModel(
+ 20L,
+ walletOwner.id(),
+ "main",
+ currency.getSymbol(),
+ BigDecimal.valueOf(0.5)
+ )
+ )
+ }
+ stubbing(walletOwnerRepository) {
+ on {
+ findById(walletOwner.id())
+ } doReturn Mono.just(
+ WalletOwnerModel(
+ walletOwner.id(),
+ walletOwner.uuid(),
+ walletOwner.title(),
+ walletOwner.level(),
+ walletOwner.isTradeAllowed(),
+ walletOwner.isWithdrawAllowed(),
+ walletOwner.isDepositAllowed()
+ )
+ )
+ }
+ stubbing(currencyRepository) {
+ on {
+ findById(currency.getSymbol())
+ } doReturn Mono.just(
+ CurrencyModel(
+ currency.getSymbol(),
+ currency.getName(),
+ currency.getPrecision()
+ )
+ )
+ }
+ val wallet = walletManagerImpl.findWalletById(20)
+
+ assertThat(wallet).isNotNull
+ assertThat(wallet!!.id()).isEqualTo(20)
+ assertThat(wallet.balance()).isEqualTo(Amount(currency, BigDecimal.valueOf(0.5)))
+ assertThat(wallet.currency().getSymbol()).isEqualTo("ETH")
+ }
+}
diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/test/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerTest.kt b/wallet/wallet-ports/wallet-persister-postgres/src/test/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerTest.kt
new file mode 100644
index 000000000..b29f7554e
--- /dev/null
+++ b/wallet/wallet-ports/wallet-persister-postgres/src/test/kotlin/co/nilin/opex/wallet/ports/postgres/impl/WalletOwnerManagerTest.kt
@@ -0,0 +1,318 @@
+package co.nilin.opex.wallet.ports.postgres.impl
+
+import co.nilin.opex.wallet.core.model.Amount
+import co.nilin.opex.wallet.core.model.Currency
+import co.nilin.opex.wallet.core.model.WalletOwner
+import co.nilin.opex.wallet.ports.postgres.dao.TransactionRepository
+import co.nilin.opex.wallet.ports.postgres.dao.UserLimitsRepository
+import co.nilin.opex.wallet.ports.postgres.dao.WalletConfigRepository
+import co.nilin.opex.wallet.ports.postgres.dao.WalletOwnerRepository
+import co.nilin.opex.wallet.ports.postgres.model.UserLimitsModel
+import co.nilin.opex.wallet.ports.postgres.model.WalletConfigModel
+import co.nilin.opex.wallet.ports.postgres.model.WalletOwnerModel
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.runBlocking
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.junit.jupiter.api.Test
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.kotlin.*
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
+import java.math.BigDecimal
+
+private class WalletOwnerManagerTest {
+ private val userLimitsRepository: UserLimitsRepository = mock()
+ private val transactionRepository: TransactionRepository = mock()
+ private val walletOwnerRepository: WalletOwnerRepository = mock()
+ private val walletConfigRepository: WalletConfigRepository = mock()
+ private val walletOwnerManagerImpl: WalletOwnerManagerImpl = WalletOwnerManagerImpl(
+ userLimitsRepository, transactionRepository, walletConfigRepository, walletOwnerRepository
+ )
+
+ private val walletOwner = object : WalletOwner {
+ override fun id() = 2L
+ override fun uuid() = "fdf453d7-0633-4ec7-852d-a18148c99a82"
+ override fun title() = "wallet"
+ override fun level() = "1"
+ override fun isTradeAllowed() = true
+ override fun isWithdrawAllowed() = true
+ override fun isDepositAllowed() = true
+ }
+
+ private val currency = object : Currency {
+ override fun getSymbol() = "ETH"
+ override fun getName() = "Ethereum"
+ override fun getPrecision() = 0.0001
+ }
+
+ @Test
+ fun givenOwnerWithNoLimit_whenIsWithdrawAllowed_thenReturnTrue(): Unit = runBlocking {
+ stubbing(userLimitsRepository) {
+ on { findByOwnerAndAction(walletOwner.id(), "withdraw") } doReturn flow { }
+ on { findByLevelAndAction(eq("1"), eq("withdraw")) } doReturn flow {}
+ }
+ stubbing(walletConfigRepository) {
+ on { findAll() } doReturn Flux.just(WalletConfigModel("default", "ETH"))
+ }
+ stubbing(transactionRepository) {
+ on {
+ calculateWithdrawStatisticsBasedOnCurrency(anyLong(), anyString(), any(), any(), anyString())
+ } doReturn Mono.empty()
+ }
+
+ val isAllowed = walletOwnerManagerImpl.isWithdrawAllowed(walletOwner, Amount(currency, BigDecimal.valueOf(0.5)))
+
+ assertThat(isAllowed).isTrue()
+ }
+
+ @Test
+ fun givenNoLimit_whenIsWithdrawAllowed_thenReturnFalse(): Unit = runBlocking {
+ stubbing(userLimitsRepository) {
+ on { findByOwnerAndAction(walletOwner.id(), "withdraw") } doReturn flow { }
+ on { findByLevelAndAction(eq("1"), eq("withdraw")) } doReturn flow {}
+ }
+ stubbing(walletConfigRepository) {
+ on { findAll() } doReturn Flux.just(WalletConfigModel("default", "ETH"))
+ }
+ stubbing(transactionRepository) {
+ on {
+ calculateWithdrawStatisticsBasedOnCurrency(anyLong(), anyString(), any(), any(), anyString())
+ } doReturn Mono.empty()
+ }
+
+ assertThatThrownBy {
+ runBlocking {
+ walletOwnerManagerImpl.isWithdrawAllowed(
+ walletOwner,
+ Amount(currency, BigDecimal.valueOf(-5))
+ )
+ }
+ }.isNotInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun givenOwnerWithLimit_whenIsWithdrawAllowedWithInvalidAmount_thenReturnFalse(): Unit = runBlocking {
+ stubbing(userLimitsRepository) {
+ on { findByOwnerAndAction(walletOwner.id(), "withdraw") } doReturn flow {
+ emit(
+ UserLimitsModel(
+ 1,
+ null,
+ walletOwner.id(),
+ "withdraw",
+ "main",
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ }
+ on { findByLevelAndAction(eq("1"), eq("withdraw")) } doReturn flow { }
+ }
+ stubbing(walletConfigRepository) {
+ on { findAll() } doReturn Flux.just(WalletConfigModel("default", "ETH"))
+ }
+ stubbing(transactionRepository) {
+ on {
+ calculateWithdrawStatisticsBasedOnCurrency(anyLong(), anyString(), any(), any(), anyString())
+ } doReturn Mono.empty()
+ }
+
+ val isAllowed =
+ walletOwnerManagerImpl.isWithdrawAllowed(walletOwner, Amount(currency, BigDecimal.valueOf(120)))
+
+ assertThat(isAllowed).isFalse()
+ }
+
+ @Test
+ fun givenLevelWithLimit_whenIsWithdrawAllowedInvalidAmount_thenReturnFalse(): Unit = runBlocking {
+ stubbing(userLimitsRepository) {
+ on { findByOwnerAndAction(walletOwner.id(), "withdraw") } doReturn flow { }
+ on { findByLevelAndAction(eq("1"), eq("withdraw")) } doReturn flow {
+ emit(
+ UserLimitsModel(
+ 1,
+ "1",
+ null,
+ "withdraw",
+ "main",
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ }
+ }
+ stubbing(walletConfigRepository) {
+ on { findAll() } doReturn Flux.just(WalletConfigModel("default", "ETH"))
+ }
+ stubbing(transactionRepository) {
+ on {
+ calculateWithdrawStatisticsBasedOnCurrency(anyLong(), anyString(), any(), any(), anyString())
+ } doReturn Mono.empty()
+ }
+
+ val isAllowed =
+ walletOwnerManagerImpl.isWithdrawAllowed(walletOwner, Amount(currency, BigDecimal.valueOf(120)))
+
+ assertThat(isAllowed).isFalse()
+ }
+
+ @Test
+ fun givenOwnerWithNoLimit_whenIsDepositAllowed_thenReturnTrue(): Unit = runBlocking {
+ stubbing(userLimitsRepository) {
+ on { findByOwnerAndAction(walletOwner.id(), "deposit") } doReturn flow { }
+ on { findByLevelAndAction(eq("1"), eq("deposit")) } doReturn flow {}
+ }
+ stubbing(walletConfigRepository) {
+ on { findAll() } doReturn Flux.just(WalletConfigModel("default", "ETH"))
+ }
+ stubbing(transactionRepository) {
+ on {
+ calculateDepositStatisticsBasedOnCurrency(anyLong(), anyString(), any(), any(), anyString())
+ } doReturn Mono.empty()
+ }
+
+ val isAllowed = walletOwnerManagerImpl.isDepositAllowed(walletOwner, Amount(currency, BigDecimal.valueOf(0.5)))
+
+ assertThat(isAllowed).isTrue()
+ }
+
+ @Test
+ fun givenWrongAmount_whenIsDepositAllowed_thenReturnTrue(): Unit = runBlocking {
+ stubbing(userLimitsRepository) {
+ on { findByOwnerAndAction(walletOwner.id(), "deposit") } doReturn flow { }
+ on { findByLevelAndAction(eq("1"), eq("deposit")) } doReturn flow {}
+ }
+ stubbing(walletConfigRepository) {
+ on { findAll() } doReturn Flux.just(WalletConfigModel("default", "ETH"))
+ }
+ stubbing(transactionRepository) {
+ on {
+ calculateDepositStatisticsBasedOnCurrency(anyLong(), anyString(), any(), any(), anyString())
+ } doReturn Mono.empty()
+ }
+
+ val isAllowed = walletOwnerManagerImpl.isDepositAllowed(walletOwner, Amount(currency, BigDecimal.valueOf(-5)))
+
+ assertThat(isAllowed).isTrue()
+ }
+
+ @Test
+ fun givenOwnerWithLimit_whenIsDepositAllowedInvalidAmount_thenReturnFalse(): Unit = runBlocking {
+ stubbing(userLimitsRepository) {
+ on { findByOwnerAndAction(walletOwner.id(), "deposit") } doReturn flow {
+ emit(
+ UserLimitsModel(
+ 1,
+ null,
+ walletOwner.id(),
+ "deposit",
+ "main",
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ }
+ on { findByLevelAndAction(eq("1"), eq("deposit")) } doReturn flow { }
+ }
+ stubbing(walletConfigRepository) {
+ on { findAll() } doReturn Flux.just(WalletConfigModel("default", "ETH"))
+ }
+ stubbing(transactionRepository) {
+ on {
+ calculateDepositStatisticsBasedOnCurrency(anyLong(), anyString(), any(), any(), anyString())
+ } doReturn Mono.empty()
+ }
+
+ val isAllowed =
+ walletOwnerManagerImpl.isDepositAllowed(walletOwner, Amount(currency, BigDecimal.valueOf(120)))
+
+ assertThat(isAllowed).isFalse()
+ }
+
+ @Test
+ fun givenLevelWithLimit_whenIsDepositAllowedInvalidAmount_thenReturnFalse(): Unit = runBlocking {
+ stubbing(userLimitsRepository) {
+ on { findByOwnerAndAction(walletOwner.id(), "deposit") } doReturn flow { }
+ on { findByLevelAndAction(eq("1"), eq("deposit")) } doReturn flow {
+ emit(
+ UserLimitsModel(
+ 1,
+ "1",
+ null,
+ "deposit",
+ "main",
+ BigDecimal.valueOf(100),
+ 10,
+ BigDecimal.valueOf(3000),
+ 300
+ )
+ )
+ }
+ }
+ stubbing(walletConfigRepository) {
+ on { findAll() } doReturn Flux.just(WalletConfigModel("default", "ETH"))
+ }
+ stubbing(transactionRepository) {
+ on {
+ calculateDepositStatisticsBasedOnCurrency(anyLong(), anyString(), any(), any(), anyString())
+ } doReturn Mono.empty()
+ }
+
+ val isAllowed =
+ walletOwnerManagerImpl.isDepositAllowed(walletOwner, Amount(currency, BigDecimal.valueOf(120)))
+
+ assertThat(isAllowed).isFalse()
+ }
+
+ @Test
+ fun givenWalletOwner_whenFindWalletOwner_thenReturnWalletOwner(): Unit = runBlocking {
+ stubbing(walletOwnerRepository) {
+ on { findByUuid(walletOwner.uuid()) } doReturn Mono.just(
+ WalletOwnerModel(
+ walletOwner.id(),
+ walletOwner.uuid(),
+ walletOwner.title(),
+ walletOwner.level(),
+ walletOwner.isTradeAllowed(),
+ walletOwner.isWithdrawAllowed(),
+ walletOwner.isDepositAllowed()
+ )
+ )
+ }
+
+ val wo = walletOwnerManagerImpl.findWalletOwner(walletOwner.uuid())
+
+ assertThat(wo!!.id()).isEqualTo(walletOwner.id())
+ assertThat(wo.uuid()).isEqualTo(walletOwner.uuid())
+ }
+
+ @Test
+ fun givenWalletOwner_whenCreateWalletOwner_thenReturnWalletOwner(): Unit = runBlocking {
+ stubbing(walletOwnerRepository) {
+ on { save(any()) } doReturn Mono.just(
+ WalletOwnerModel(
+ walletOwner.id(),
+ walletOwner.uuid(),
+ walletOwner.title(),
+ walletOwner.level(),
+ walletOwner.isTradeAllowed(),
+ walletOwner.isWithdrawAllowed(),
+ walletOwner.isDepositAllowed()
+ )
+ )
+ }
+
+ val wo = walletOwnerManagerImpl.createWalletOwner(walletOwner.uuid(), walletOwner.title(), walletOwner.level())
+
+ assertThat(wo.id()).isEqualTo(walletOwner.id())
+ assertThat(wo.uuid()).isEqualTo(walletOwner.uuid())
+ }
+}