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 }