libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

bench.kt (16131B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2024-2025 Taler Systems S.A.
      4 
      5  * LibEuFin is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU Affero General Public License as
      7  * published by the Free Software Foundation; either version 3, or
      8  * (at your option) any later version.
      9 
     10  * LibEuFin is distributed in the hope that it will be useful, but
     11  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13  * Public License for more details.
     14 
     15  * You should have received a copy of the GNU Affero General Public
     16  * License along with LibEuFin; see the file COPYING.  If not, see
     17  * <http://www.gnu.org/licenses/>
     18  */
     19 
     20 import io.ktor.client.request.*
     21 import io.ktor.http.*
     22 import org.junit.Test
     23 import org.postgresql.jdbc.PgConnection
     24 import tech.libeufin.bank.*
     25 import tech.libeufin.common.*
     26 import tech.libeufin.common.crypto.PwCrypto
     27 import tech.libeufin.common.test.*
     28 import java.time.Instant
     29 import java.time.LocalDateTime
     30 import java.time.ZoneId
     31 import java.util.*
     32 import kotlin.math.max
     33 
     34 class Bench {
     35 
     36     /** Generate [amount] rows to fill the database */
     37     fun genData(conn: PgConnection, amount: Int) {
     38         val amount = max(amount, 10)
     39 
     40         // Skip 4 accounts created by bankSetup
     41         val skipAccount = 4 
     42         // Customer account will be used in tests so we want to generate more data for him
     43         val customerAccount = 3
     44         val exchangeAccount = 2
     45         // In general half of the data is for generated account and half is for customer
     46         val mid = amount / 2
     47 
     48         val password = PwCrypto.Bcrypt(cost = 4).hashpw("password")
     49         
     50         val token16 = ByteArray(16)
     51         val token32 = ByteArray(32)
     52         val token64 = ByteArray(64)
     53 
     54         conn.genData(amount, sequenceOf(
     55             "customers(username, name, password_hash, cashout_payto)" to {
     56                 "account_$it\t$password\tMr n°$it\t$unknownPayto\n"
     57             },
     58             "conversion_rate_classes(name)" to {
     59                 "Class n0$it\n"
     60             },
     61             "bank_accounts(internal_payto, owning_customer_id, is_public,conversion_rate_class_id)" to {
     62                 val conversionId = when (it%5) {
     63                     0 -> "\\N"
     64                     1, 2 -> "1"
     65                     3 -> "2"
     66                     else -> it%10
     67                 }
     68                 "payto://x-taler-bank/localhost/account_$it\t${it+skipAccount}\t${it%3==0}\t$conversionId\n"
     69             },
     70             "bearer_tokens(content, creation_time, expiration_time, scope, is_refreshable, bank_customer, description, last_access)" to {
     71                 val account = if (it > mid) customerAccount else it+4
     72                 val hex = token32.rand().encodeHex()
     73                 "\\\\x$hex\t0\t0\treadonly\tfalse\t$account\t\\N\t0\n"
     74             },
     75             "bank_account_transactions(creditor_payto, creditor_name, debtor_payto, debtor_name, subject, amount, transaction_date, direction, bank_account_id)" to {
     76                 val account = if (it > mid) customerAccount else it+4
     77                 "$unknownPayto\tcreditor_name\t$unknownPayto\tdebtor_name\tsubject\t(42,0)\t0\tcredit\t$exchangeAccount\n" + 
     78                 "$unknownPayto\tcreditor_name\t$unknownPayto\tdebtor_name\tsubject\t(42,0)\t0\tdebit\t$account\n"
     79             },
     80             "bank_transaction_operations" to {
     81                 val hex = token32.rand().encodeHex()
     82                 "\\\\x$hex\t$it\n"
     83             },
     84             "tan_challenges(uuid, hbody, salt, op, code, creation_date, expiration_date, retry_counter, customer, tan_channel, tan_info)" to {
     85                 val account = if (it > mid) customerAccount else it+4
     86                 val uuid = UUID.randomUUID()
     87                 val hex16 = token16.rand().encodeHex()
     88                 val hex64 = token64.rand().encodeHex()
     89                 "$uuid\t\\\\x$hex64\t\\\\x$hex16\taccount_reconfig\tcode\t0\t0\t0\t$account\tsms\tinfo\n"
     90             },
     91             "taler_withdrawal_operations(withdrawal_uuid, wallet_bank_account, reserve_pub, creation_date)" to {
     92                 val account = if (it > mid) customerAccount else it+4
     93                 val hex = token32.rand().encodeHex()
     94                 val uuid = UUID.randomUUID()
     95                 "$uuid\t$account\t\\\\x$hex\t0\n"
     96             },
     97             "taler_exchange_outgoing(bank_transaction)" to {
     98                 "${it*2-1}\n"
     99             },
    100             "transfer_operations(wtid, request_uid, amount, exchange_base_url, exchange_outgoing_id, exchange_id, transfer_date, creditor_payto, status, status_msg)" to {
    101                 val hex32 = token32.rand().encodeHex()
    102                 val hex64 = token64.rand().encodeHex()
    103                 if (it % 2 == 0) {
    104                     "\\\\x$hex32\t\\\\x$hex64\t(42, 0)\turl\t$it\t$it\t0\tpayto://x-taler-bank/localhost/10\tsuccess\t\\N\n"
    105                 } else {
    106                     "\\\\x$hex32\t\\\\x$hex64\t(42, 0)\turl\t\\N\t$it\t0\tpayto://x-taler-bank/localhost/10\tpermanent_failure\tfailure\n"
    107                 }   
    108             },
    109             "taler_exchange_incoming(type, metadata, bank_transaction)" to {
    110                 val hex = token32.rand().encodeHex()
    111                 if (it % 2 == 0) {
    112                     "reserve\t\\\\x$hex\t${it*2}\n"
    113                 } else {
    114                     "kyc\t\\\\x$hex\t${it*2}\n"
    115                 }
    116             },
    117             "bank_stats(timeframe, start_time)" to {
    118                 val instant = Instant.ofEpochSecond(it.toLong())
    119                 val date = LocalDateTime.ofInstant(instant, ZoneId.of("UTC"))
    120                 "day\t$date\n"
    121             },
    122             "cashout_operations(request_uid,amount_debit,amount_credit,subject,creation_time,bank_account,local_transaction)" to {
    123                 val account = if (it > mid) customerAccount else it+4
    124                 val hex = token32.rand().encodeHex()
    125                 "\\\\x$hex\t(0,0)\t(0,0)\tsubject\t0\t$account\t$it\n"
    126             }
    127         ))
    128     }
    129 
    130     @Test
    131     fun benchDb() = bench { AMOUNT -> bankSetup { db ->
    132         // Prepare custoemr accounts
    133         fillCashoutInfo("customer")
    134         setMaxDebt("customer", "KUDOS:1000000")
    135 
    136         // Generate data
    137         db.conn { genData(it, AMOUNT) }
    138 
    139         // Warm HTTP client
    140         client.get("/config").assertOk()
    141         
    142         // Accounts
    143         measureAction("account_create") {
    144             client.post("/accounts") {
    145                 json {
    146                     "username" to "account_bench_$it"
    147                     "password" to "account_bench_$it-password"
    148                     "name" to "Bench Account $it"
    149                 }
    150             }.assertOkJson<RegisterAccountResponse>().internal_payto_uri
    151         }
    152         measureAction("account_reconfig") {
    153             client.patchA("/accounts/account_bench_$it") {
    154                 json {
    155                     "name" to "New Bench Account $it"
    156                 }
    157             }.assertNoContent()
    158         }
    159         measureAction("account_reconfig_auth") {
    160             client.patchA("/accounts/account_bench_$it/auth") {
    161                 json {
    162                     "old_password" to "account_bench_$it-password"
    163                     "new_password" to "account_bench_$it-password"
    164                 }
    165             }.assertNoContent()
    166         }
    167         measureAction("account_list") {
    168             client.getAdmin("/accounts").assertOk()
    169         }
    170         measureAction("account_list_class") {
    171             client.getAdmin("/accounts?conversion_rate_class=${it%10}").assertOk()
    172         }
    173         measureAction("account_list_name") {
    174             client.getAdmin("/accounts?name=Mr").assertOk()
    175         }
    176         measureAction("account_list_public") {
    177             client.get("/public-accounts").assertOk()
    178         }
    179         measureAction("account_get") {
    180             client.getA("/accounts/account_bench_$it").assertOk()
    181         }
    182         
    183         // Tokens
    184         val tokens = measureAction("token_create") {
    185             client.postPw("/accounts/customer/token") {
    186                 json { 
    187                     "scope" to "readonly"
    188                     "refreshable" to true
    189                 }
    190             }.assertOkJson<TokenSuccessResponse>().access_token
    191         }
    192         measureAction("token_refresh") {
    193             client.post("/accounts/customer/token") {
    194                 headers[HttpHeaders.Authorization] = "Bearer ${tokens[it]}"
    195                 json { "scope" to "readonly" }
    196             }.assertOk()
    197         }
    198         measureAction("token_list") {
    199             client.getA("/accounts/customer/tokens").assertOk()
    200         }
    201         measureAction("token_delete") {
    202             client.delete("/accounts/customer/token") {
    203                 headers[HttpHeaders.Authorization] = "Bearer ${tokens[it]}"
    204             }.assertNoContent()
    205         }
    206 
    207         // Conversion rate classes
    208         val classes = measureAction("class_create") {
    209             client.postAdmin("/conversion-rate-classes") {
    210                 json { 
    211                     "name" to "Gen class $it"
    212                 }
    213             }.assertOkJson<ConversionRateClassResponse>().conversion_rate_class_id
    214         }
    215         measureAction("class_patch") {
    216             client.patchAdmin("/conversion-rate-classes/${classes[it]}") {
    217                 json {
    218                     "name" to "Gen class $it"
    219                     "description" to "test $it"
    220                 }
    221             }.assertNoContent()
    222         }
    223         measureAction("class_get") {
    224             client.getAdmin("/conversion-rate-classes/${classes[it]}").assertOk()
    225         }
    226         measureAction("class_list") {
    227             client.getAdmin("/conversion-rate-classes").assertOk()
    228         }
    229         measureAction("class_delete") {
    230             client.deleteAdmin("/conversion-rate-classes/${classes[it]}").assertNoContent()
    231         }
    232 
    233         // Transaction
    234         val transactions = measureAction("transaction_create") {
    235             client.postA("/accounts/customer/transactions") {
    236                 json { 
    237                     "payto_uri" to "$merchantPayto?receiver-name=Test&message=payout"
    238                     "amount" to "KUDOS:0.0001"
    239                 }
    240             }.assertOkJson<TransactionCreateResponse>().row_id
    241         }
    242         measureAction("transaction_get") {
    243             client.getA("/accounts/customer/transactions/${transactions[it]}").assertOk()
    244         }
    245         measureAction("transaction_history") {
    246             client.getA("/accounts/customer/transactions").assertOk()
    247         }
    248         measureAction("transaction_revenue") {
    249             client.getA("/accounts/merchant/taler-revenue/history").assertOk()
    250         }
    251 
    252         // Withdrawal
    253         val withdrawals = measureAction("withdrawal_create") {
    254             client.postA("/accounts/customer/withdrawals") {
    255                 json { 
    256                     "amount" to "KUDOS:0.0001"
    257                 }
    258             }.assertOkJson<BankAccountCreateWithdrawalResponse>().withdrawal_id
    259         }
    260         measureAction("withdrawal_get") {
    261             client.get("/withdrawals/${withdrawals[it]}").assertOk()
    262         }
    263         measureAction("withdrawal_status") {
    264             client.get("/taler-integration/withdrawal-operation/${withdrawals[it]}").assertOk()
    265         }
    266         measureAction("withdrawal_select") {
    267             client.post("/taler-integration/withdrawal-operation/${withdrawals[it]}") {
    268                 json {
    269                     "reserve_pub" to EddsaPublicKey.rand()
    270                     "selected_exchange" to exchangePayto
    271                 }
    272             }.assertOk()
    273         }
    274         measureAction("withdrawal_confirm") {
    275             client.postA("/accounts/customer/withdrawals/${withdrawals[it]}/confirm")
    276                 .assertNoContent()
    277         }
    278         measureAction("withdrawal_abort") {
    279             val uuid = client.postA("/accounts/customer/withdrawals") {
    280                 json { 
    281                     "amount" to "KUDOS:0.0001"
    282                 }
    283             }.assertOkJson<BankAccountCreateWithdrawalResponse>().withdrawal_id
    284             client.postA("/accounts/customer/withdrawals/$uuid/abort")
    285                 .assertNoContent()
    286         }
    287 
    288         // Cashout
    289         convert("KUDOS:0.1")
    290         val cashouts = measureAction("cashout_create") {
    291             client.postA("/accounts/customer/cashouts") {
    292                 json { 
    293                     "request_uid" to ShortHashCode.rand()
    294                     "amount_debit" to "KUDOS:0.1"
    295                     "amount_credit" to convert("KUDOS:0.1")
    296                 }
    297             }.assertOkJson<CashoutResponse>().cashout_id
    298         }
    299         measureAction("cashout_get") {
    300             client.getA("/accounts/customer/cashouts/${cashouts[it]}").assertOk()
    301         }
    302         measureAction("cashout_history") {
    303             client.getA("/accounts/customer/cashouts").assertOk()
    304         }
    305         measureAction("cashout_history_admin") {
    306             client.getAdmin("/cashouts").assertOk()
    307         }
    308 
    309         // Wire gateway
    310         val transfers = measureAction("wg_transfer") {
    311             client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
    312                 json { 
    313                     "request_uid" to HashCode.rand()
    314                     "amount" to "KUDOS:0.0001"
    315                     "exchange_base_url" to "http://exchange.example.com/"
    316                     "wtid" to ShortHashCode.rand()
    317                     "credit_account" to customerPayto.canonical
    318                 }
    319             }.assertOkJson<TransferResponse>().row_id
    320         }
    321         measureAction("wg_transfer_get") {
    322             client.getA("/accounts/exchange/taler-wire-gateway/transfers/${transfers[it]}").assertOk()
    323         }
    324         measureAction("wg_transfer_page") {
    325             client.getA("/accounts/exchange/taler-wire-gateway/transfers").assertOk()
    326         }
    327         measureAction("wg_transfer_page_filter") {
    328             client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=success").assertOk()
    329         }
    330         measureAction("wg_add") {
    331             client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") {
    332                 json { 
    333                     "amount" to "KUDOS:0.0001"
    334                     "reserve_pub" to EddsaPublicKey.rand()
    335                     "debit_account" to customerPayto.canonical
    336                 }
    337             }.assertOk()
    338         }
    339         measureAction("wg_incoming") {
    340             client.getA("/accounts/exchange/taler-wire-gateway/history/incoming")
    341                 .assertOk()
    342         }
    343         measureAction("wg_outgoing") {
    344             client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing")
    345                 .assertOk()
    346         }
    347 
    348         // TAN challenges
    349         val challenges = measureAction("tan_send") {
    350             val res = client.patchA("/accounts/account_bench_$it") {
    351                 json { 
    352                     "contact_data" to obj {
    353                         "phone" to "+99"
    354                         "email" to "email@example.com"
    355                     }
    356                     "tan_channel" to "sms"
    357                 }
    358             }.assertAcceptedJson<ChallengeResponse>()
    359             val challenge = res.challenges[0]
    360             client.postA("/accounts/account_bench_$it/challenge/${challenge.challenge_id}").assertOk()
    361             val code = tanCode(challenge.tan_info)
    362             Pair(challenge.challenge_id, code)
    363         }
    364         measureAction("tan_confirm") {
    365             val (id, code) = challenges[it]
    366             client.postA("/accounts/account_bench_$it/challenge/$id/confirm") {
    367                 json { "tan" to code }
    368             }.assertNoContent()
    369         }
    370 
    371         // Delete accounts
    372 
    373         measureAction("account_delete") {
    374             client.deleteA("/accounts/account_bench_$it").assertNoContent()
    375         }
    376 
    377         // Other
    378         measureAction("monitor") {
    379             client.getAdmin("/monitor").assertOk()
    380         }
    381         db.gc.collect(Instant.now(), java.time.Duration.ZERO, java.time.Duration.ZERO, java.time.Duration.ZERO)
    382         measureAction("gc") {
    383             db.gc.collect(Instant.now(), java.time.Duration.ZERO, java.time.Duration.ZERO, java.time.Duration.ZERO)
    384         }
    385     } }
    386 }