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()) + } +}