libeufin

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

commit 537bb9822a3b6c2bd7f777cad72b4580118beb10
parent cdf5a033bb8705d3bcdf470cd66b41e5091e9c68
Author: Antoine A <>
Date:   Tue, 14 Nov 2023 12:35:45 +0000

Improve corebank API and tests

Diffstat:
MMakefile | 8+++++---
Mbank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt | 32+++++++++++++-------------------
Mbank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt | 73+++++++++++++++++++++----------------------------------------------------
Mbank/src/main/kotlin/tech/libeufin/bank/Params.kt | 11+++++++++++
Mbank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt | 60++++++------------------------------------------------------
Mbank/src/main/kotlin/tech/libeufin/bank/db/Database.kt | 137+++++++++++++++++++++++++++++++------------------------------------------------
Mbank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt | 47+++++++++++++++++++++++++++++++++++++----------
Mbank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 16----------------
Mbank/src/test/kotlin/AmountTest.kt | 2+-
Mbank/src/test/kotlin/CoreBankApiTest.kt | 137++++++++++++++++++++++++++++---------------------------------------------------
Mbank/src/test/kotlin/SecurityTest.kt | 2+-
Mbank/src/test/kotlin/WireGatewayApiTest.kt | 2+-
Mbank/src/test/kotlin/helpers.kt | 11+++++++++++
13 files changed, 209 insertions(+), 329 deletions(-)

diff --git a/Makefile b/Makefile @@ -36,13 +36,15 @@ deb: .PHONY: install install: install-bank -.PHONY: install-bank -install-bank: +install-bank-files: install -d $(bank_config_dir) install contrib/libeufin-bank.conf $(bank_config_dir)/ install contrib/currencies.conf $(bank_config_dir)/ install -D database-versioning/libeufin-bank*.sql -t $(bank_sql_dir) install -D database-versioning/versioning.sql -t $(bank_sql_dir) + +.PHONY: install-bank +install-bank: install-bank-files install -d $(spa_dir) cp contrib/wallet-core/demobank/* $(spa_dir)/ ./gradlew bank:installShadowDist @@ -63,5 +65,5 @@ assemble: ./gradlew assemble .PHONY: check -check: +check: install-bank-files ./gradlew check diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt @@ -39,26 +39,20 @@ fun Routing.bankIntegrationApi(db: Database, ctx: BankConfig) { // Note: wopid acts as an authentication token. get("/taler-integration/withdrawal-operation/{wopid}") { // TODO long poll - val op = call.getWithdrawal(db, "wopid") // throws 404 if not found. - val relatedBankAccount = db.bankAccountGetFromOwnerId(op.walletBankAccount) - ?: throw internalServerError("Bank has a withdrawal not related to any bank account.") - val suggestedExchange = ctx.suggestedWithdrawalExchange - val confirmUrl = if (ctx.spaCaptchaURL == null) null else - getWithdrawalConfirmUrl( - baseUrl = ctx.spaCaptchaURL, - wopId = op.withdrawalUuid - ) - call.respond( - BankWithdrawalOperationStatus( - aborted = op.aborted, - selection_done = op.selectionDone, - transfer_done = op.confirmationDone, - amount = op.amount, - sender_wire = relatedBankAccount.internalPaytoUri.canonical, - suggested_exchange = suggestedExchange, - confirm_transfer_url = confirmUrl - ) + val uuid = call.uuidUriComponent("wopid") + val op = db.withdrawal.getStatus(uuid) ?: throw notFound( + "Withdrawal operation '$uuid' not found", + TalerErrorCode.BANK_TRANSACTION_NOT_FOUND ) + call.respond(op.copy( + suggested_exchange = ctx.suggestedWithdrawalExchange, + confirm_transfer_url = ctx.spaCaptchaURL?.run { + getWithdrawalConfirmUrl( + baseUrl = this, + wopId = uuid + ) + } + )) } post("/taler-integration/withdrawal-operation/{wopid}") { val opId = call.uuidUriComponent("wopid") diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -61,7 +61,7 @@ fun Routing.coreBankApi(db: Database, ctx: BankConfig) { } } coreBankTokenApi(db) - coreBankAccountsMgmtApi(db, ctx) + coreBankAccountsApi(db, ctx) coreBankTransactionsApi(db, ctx) coreBankWithdrawalApi(db, ctx) coreBankCashoutApi(db, ctx) @@ -144,7 +144,7 @@ private fun Routing.coreBankTokenApi(db: Database) { } } -private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) { +private fun Routing.coreBankAccountsApi(db: Database, ctx: BankConfig) { authAdmin(db, TokenScope.readwrite, ctx.restrictRegistration) { post("/accounts") { val req = call.receive<RegisterAccountRequest>() @@ -274,7 +274,8 @@ private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) { } } get("/public-accounts") { - val publicAccounts = db.accountsGetPublic(ctx.currency) + val params = AccountParams.extract(call.request.queryParameters) + val publicAccounts = db.accountsGetPublic(params) if (publicAccounts.isEmpty()) { call.respond(HttpStatusCode.NoContent) } else { @@ -283,14 +284,8 @@ private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) { } authAdmin(db, TokenScope.readonly) { get("/accounts") { - // Get optional param. - val maybeFilter: String? = call.request.queryParameters["filter_name"] - logger.debug("Filtering on '${maybeFilter}'") - val queryParam = - if (maybeFilter != null) { - "%${maybeFilter}%" - } else "%" - val accounts = db.accountsGetForAdmin(queryParam) + val params = AccountParams.extract(call.request.queryParameters) + val accounts = db.accountsGetForAdmin(params) if (accounts.isEmpty()) { call.respond(HttpStatusCode.NoContent) } else { @@ -324,46 +319,25 @@ private fun Routing.coreBankTransactionsApi(db: Database, ctx: BankConfig) { } } get("/accounts/{USERNAME}/transactions/{T_ID}") { - val tId = call.expectUriComponent("T_ID") - val txRowId = - try { - tId.toLong() - } catch (e: Exception) { - logger.error(e.message) - throw badRequest("TRANSACTION_ID is not a number: ${tId}") - } - + val tId = call.longUriComponent("T_ID") val bankAccount = call.bankAccount(db) - val tx = - db.bankTransactionGetFromInternalId(txRowId) - ?: throw notFound( - "Bank transaction '$tId' not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) - if (tx.bankAccountId != bankAccount.bankAccountId) // TODO not found ? - throw unauthorized("Client has no rights over the bank transaction: $tId") - - call.respond( - BankAccountTransactionInfo( - amount = tx.amount, - creditor_payto_uri = tx.creditorPaytoUri, - debtor_payto_uri = tx.debtorPaytoUri, - date = TalerProtocolTimestamp(tx.transactionDate), - direction = tx.direction, - subject = tx.subject, - row_id = txRowId + val (tx, accountId) = db.bankTransactionGetFromInternalId(tId) ?: throw notFound( + "Bank transaction '$tId' not found", + TalerErrorCode.BANK_TRANSACTION_NOT_FOUND ) - ) + if (accountId != bankAccount.bankAccountId) // TODO not found ? + throw unauthorized("Client has no rights over the bank transaction: $tId") + call.respond(tx) } } auth(db, TokenScope.readwrite) { post("/accounts/{USERNAME}/transactions") { - val tx = call.receive<BankAccountTransactionCreate>() + val tx = call.receive<TransactionCreateRequest>() val subject = tx.payto_uri.message ?: throw badRequest("Wire transfer lacks subject") val amount = tx.payto_uri.amount ?: tx.amount ?: throw badRequest("Wire transfer lacks amount") ctx.checkRegionalCurrency(amount) - val result = db.bankTransaction( + val (result, id) = db.bankTransaction( creditAccountPayto = tx.payto_uri, debitAccountUsername = username, subject = subject, @@ -387,7 +361,7 @@ private fun Routing.coreBankTransactionsApi(db: Database, ctx: BankConfig) { "Insufficient funds", TalerErrorCode.BANK_UNALLOWED_DEBIT ) - BankTransactionResult.SUCCESS -> call.respond(HttpStatusCode.NoContent) + BankTransactionResult.SUCCESS -> call.respond(TransactionCreateResponse(id!!)) } } } @@ -426,17 +400,12 @@ private fun Routing.coreBankWithdrawalApi(db: Database, ctx: BankConfig) { } } get("/withdrawals/{withdrawal_id}") { - val op = call.getWithdrawal(db, "withdrawal_id") - call.respond( - BankAccountGetWithdrawalResponse( - amount = op.amount, - aborted = op.aborted, - confirmation_done = op.confirmationDone, - selection_done = op.selectionDone, - selected_exchange_account = op.selectedExchangePayto, - selected_reserve_pub = op.reservePub - ) + val uuid = call.uuidUriComponent("withdrawal_id") + val op = db.withdrawal.get(uuid) ?: throw notFound( + "Withdrawal operation '$uuid' not found", + TalerErrorCode.BANK_TRANSACTION_NOT_FOUND ) + call.respond(op) } post("/withdrawals/{withdrawal_id}/abort") { val opId = call.uuidUriComponent("withdrawal_id") diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Params.kt b/bank/src/main/kotlin/tech/libeufin/bank/Params.kt @@ -93,6 +93,17 @@ data class HistoryParams( } } +data class AccountParams( + val page: PageParams, val loginFilter: String +) { + companion object { + fun extract(params: Parameters): AccountParams { + val loginFilter = params.get("filter_name")?.run { "%$this%" } ?: "%" + return AccountParams(PageParams.extract(params), loginFilter) + } + } +} + data class PageParams( val delta: Int, val start: Long ) { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -162,14 +162,8 @@ data class MonitorWithConversion( */ data class BankAccount( val internalPaytoUri: IbanPayTo, - // Database row ID of the customer that owns this bank account. - val owningCustomerId: Long, val bankAccountId: Long, - val isPublic: Boolean, val isTalerExchange: Boolean, - val balance: TalerAmount, - val hasDebt: Boolean, - val maxDebt: TalerAmount ) // Allowed values for cashout TAN channels. @@ -205,50 +199,6 @@ data class BearerToken( val bankCustomer: Long ) -/** - * Convenience type representing bank transactions as they - * are in the respective database table. Only used to _get_ - * the information from the database. - */ -data class BankAccountTransaction( - val creditorPaytoUri: String, - val creditorName: String, - val debtorPaytoUri: String, - val debtorName: String, - val subject: String, - val amount: TalerAmount, - val transactionDate: Instant, - /** - * Is the transaction debit, or credit for the - * bank account pointed by this object? - */ - val direction: TransactionDirection, - /** - * database row ID of the bank account that is - * impacted by the direction. For example, if the - * direction is debit, then this value points to the - * bank account of the payer. - */ - val bankAccountId: Long, - // Null if this type is used to _create_ one transaction. - val dbRowId: Long -) - -/** - * Represents a Taler withdrawal operation, as it is - * stored in the respective database table. - */ -data class TalerWithdrawalOperation( - val withdrawalUuid: UUID, - val amount: TalerAmount, - val selectionDone: Boolean, - val aborted: Boolean, - val confirmationDone: Boolean, - val reservePub: EddsaPublicKey?, - val selectedExchangePayto: IbanPayTo?, - val walletBankAccount: Long -) - // Type to return as GET /config response @Serializable data class Config( @@ -305,15 +255,17 @@ data class AccountData( val cashout_payto_uri: IbanPayTo? = null, ) -/** - * Response type of corebank API transaction initiation. - */ @Serializable -data class BankAccountTransactionCreate( +data class TransactionCreateRequest( val payto_uri: IbanPayTo, val amount: TalerAmount? ) +@Serializable +data class TransactionCreateResponse( + val row_id: Long +) + /* History element, either from GET /transactions/T_ID or from GET /transactions */ @Serializable diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt @@ -460,8 +460,11 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val * * Returns an empty list, if no public account was found. */ - suspend fun accountsGetPublic(internalCurrency: String, loginFilter: String = "%"): List<PublicAccount> = conn { conn -> - val stmt = conn.prepareStatement(""" + suspend fun accountsGetPublic(params: AccountParams): List<PublicAccount> + = page( + params.page, + "bank_account_id", + """ SELECT (balance).val AS balance_val, (balance).frac AS balance_frac, @@ -470,10 +473,13 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val c.login FROM bank_accounts JOIN customers AS c ON owning_customer_id = c.customer_id - WHERE is_public=true AND c.login LIKE ?; - """) - stmt.setString(1, loginFilter) - stmt.all { + WHERE is_public=true AND c.login LIKE ? AND + """, + { + setString(1, params.loginFilter) + 1 + } + ) { PublicAccount( account_name = it.getString("login"), payto_uri = it.getString("internal_payto_uri"), @@ -481,7 +487,7 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val amount = TalerAmount( value = it.getLong("balance_val"), frac = it.getInt("balance_frac"), - currency = internalCurrency + currency = bankCurrency ), credit_debit_indicator = if (it.getBoolean("has_debt")) { CreditDebitInfo.debit @@ -491,7 +497,6 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val ) ) } - } /** * Gets a minimal set of account data, as outlined in the GET /accounts @@ -499,22 +504,28 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val * LIKE operator. If it's null, it defaults to the "%" wildcard, meaning * that it returns ALL the existing accounts. */ - suspend fun accountsGetForAdmin(nameFilter: String = "%"): List<AccountMinimalData> = conn { conn -> - val stmt = conn.prepareStatement(""" + suspend fun accountsGetForAdmin(params: AccountParams): List<AccountMinimalData> + = page( + params.page, + "bank_account_id", + """ SELECT - login, - name, - (b.balance).val AS balance_val, - (b.balance).frac AS balance_frac, - (b).has_debt AS balance_has_debt, - (max_debt).val as max_debt_val, - (max_debt).frac as max_debt_frac - FROM customers JOIN bank_accounts AS b - ON customer_id = b.owning_customer_id - WHERE name LIKE ?; - """) - stmt.setString(1, nameFilter) - stmt.all { + login, + name, + (b.balance).val AS balance_val, + (b.balance).frac AS balance_frac, + (b).has_debt AS balance_has_debt, + (max_debt).val as max_debt_val, + (max_debt).frac as max_debt_frac + FROM customers JOIN bank_accounts AS b + ON customer_id = b.owning_customer_id + WHERE name LIKE ? AND + """, + { + setString(1, params.loginFilter) + 1 + } + ) { AccountMinimalData( username = it.getString("login"), name = it.getString("name"), @@ -537,7 +548,6 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val ) ) } - } // BANK ACCOUNTS @@ -545,14 +555,7 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val val stmt = conn.prepareStatement(""" SELECT internal_payto_uri - ,owning_customer_id - ,is_public ,is_taler_exchange - ,(balance).val AS balance_val - ,(balance).frac AS balance_frac - ,has_debt - ,(max_debt).val AS max_debt_val - ,(max_debt).frac AS max_debt_frac ,bank_account_id FROM bank_accounts WHERE owning_customer_id=? @@ -562,20 +565,7 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val stmt.oneOrNull { BankAccount( internalPaytoUri = IbanPayTo(it.getString("internal_payto_uri")), - balance = TalerAmount( - it.getLong("balance_val"), - it.getInt("balance_frac"), - bankCurrency - ), - owningCustomerId = it.getLong("owning_customer_id"), - hasDebt = it.getBoolean("has_debt"), isTalerExchange = it.getBoolean("is_taler_exchange"), - isPublic = it.getBoolean("is_public"), - maxDebt = TalerAmount( - value = it.getLong("max_debt_val"), - frac = it.getInt("max_debt_frac"), - bankCurrency - ), bankAccountId = it.getLong("bank_account_id") ) } @@ -585,15 +575,8 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val val stmt = conn.prepareStatement(""" SELECT bank_account_id - ,owning_customer_id ,internal_payto_uri - ,is_public ,is_taler_exchange - ,(balance).val AS balance_val - ,(balance).frac AS balance_frac - ,has_debt - ,(max_debt).val AS max_debt_val - ,(max_debt).frac AS max_debt_frac FROM bank_accounts JOIN customers ON customer_id=owning_customer_id @@ -604,20 +587,7 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val stmt.oneOrNull { BankAccount( internalPaytoUri = IbanPayTo(it.getString("internal_payto_uri")), - balance = TalerAmount( - it.getLong("balance_val"), - it.getInt("balance_frac"), - bankCurrency - ), - owningCustomerId = it.getLong("owning_customer_id"), - hasDebt = it.getBoolean("has_debt"), isTalerExchange = it.getBoolean("is_taler_exchange"), - maxDebt = TalerAmount( - value = it.getLong("max_debt_val"), - frac = it.getInt("max_debt_frac"), - bankCurrency - ), - isPublic = it.getBoolean("is_public"), bankAccountId = it.getLong("bank_account_id") ) } @@ -665,7 +635,7 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val subject: String, amount: TalerAmount, timestamp: Instant, - ): BankTransactionResult = conn { conn -> + ): Pair<BankTransactionResult, Long?> = conn { conn -> conn.transaction { val stmt = conn.prepareStatement(""" SELECT @@ -689,7 +659,8 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val stmt.setInt(5, amount.frac) stmt.setLong(6, timestamp.toDbMicros() ?: throw faultyTimestampByBank()) stmt.executeQuery().use { - when { + var rowId: Long? = null; + val result = when { !it.next() -> throw internalServerError("Bank transaction didn't properly return") it.getBoolean("out_creditor_not_found") -> BankTransactionResult.NO_CREDITOR it.getBoolean("out_debtor_not_found") -> BankTransactionResult.NO_DEBTOR @@ -697,21 +668,21 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val it.getBoolean("out_balance_insufficient") -> BankTransactionResult.BALANCE_INSUFFICIENT else -> { handleExchangeTx(conn, subject, it.getLong("out_credit_bank_account_id"), it.getLong("out_debit_bank_account_id"), it) + rowId = it.getLong("out_debit_row_id"); BankTransactionResult.SUCCESS } } + Pair(result, rowId) } } } // Get the bank transaction whose row ID is rowId - suspend fun bankTransactionGetFromInternalId(rowId: Long): BankAccountTransaction? = conn { conn -> + suspend fun bankTransactionGetFromInternalId(rowId: Long): Pair<BankAccountTransactionInfo, Long>? = conn { conn -> val stmt = conn.prepareStatement(""" SELECT creditor_payto_uri - ,creditor_name ,debtor_payto_uri - ,debtor_name ,subject ,(amount).val AS amount_val ,(amount).frac AS amount_frac @@ -724,21 +695,21 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val """) stmt.setLong(1, rowId) stmt.oneOrNull { - BankAccountTransaction( - creditorPaytoUri = it.getString("creditor_payto_uri"), - creditorName = it.getString("creditor_name"), - debtorPaytoUri = it.getString("debtor_payto_uri"), - debtorName = it.getString("debtor_name"), - amount = TalerAmount( - it.getLong("amount_val"), - it.getInt("amount_frac"), - bankCurrency + Pair( + BankAccountTransactionInfo( + creditor_payto_uri = it.getString("creditor_payto_uri"), + debtor_payto_uri = it.getString("debtor_payto_uri"), + amount = TalerAmount( + it.getLong("amount_val"), + it.getInt("amount_frac"), + bankCurrency + ), + direction = TransactionDirection.valueOf(it.getString("direction")), + subject = it.getString("subject"), + date = TalerProtocolTimestamp(it.getLong("transaction_date").microsToJavaInstant() ?: throw faultyTimestampByBank()), + row_id = it.getLong("bank_transaction_id") ), - direction = TransactionDirection.valueOf(it.getString("direction")), - bankAccountId = it.getLong("bank_account_id"), - subject = it.getString("subject"), - transactionDate = it.getLong("transaction_date").microsToJavaInstant() ?: throw faultyTimestampByBank(), - dbRowId = it.getLong("bank_transaction_id") + it.getLong("bank_account_id") ) } } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt @@ -82,36 +82,63 @@ class WithdrawalDAO(private val db: Database) { } } - suspend fun get(uuid: UUID): TalerWithdrawalOperation? = db.conn { conn -> + suspend fun get(uuid: UUID): BankAccountGetWithdrawalResponse? = db.conn { conn -> val stmt = conn.prepareStatement(""" SELECT (amount).val as amount_val ,(amount).frac as amount_frac - ,withdrawal_uuid ,selection_done ,aborted ,confirmation_done ,reserve_pub ,selected_exchange_payto - ,wallet_bank_account FROM taler_withdrawal_operations WHERE withdrawal_uuid=? """) stmt.setObject(1, uuid) stmt.oneOrNull { - TalerWithdrawalOperation( + BankAccountGetWithdrawalResponse( amount = TalerAmount( it.getLong("amount_val"), it.getInt("amount_frac"), db.bankCurrency ), - selectionDone = it.getBoolean("selection_done"), - selectedExchangePayto = it.getString("selected_exchange_payto")?.run(::IbanPayTo), - walletBankAccount = it.getLong("wallet_bank_account"), - confirmationDone = it.getBoolean("confirmation_done"), + selection_done = it.getBoolean("selection_done"), + confirmation_done = it.getBoolean("confirmation_done"), aborted = it.getBoolean("aborted"), - reservePub = it.getBytes("reserve_pub")?.run(::EddsaPublicKey), - withdrawalUuid = it.getObject("withdrawal_uuid") as UUID + selected_exchange_account = it.getString("selected_exchange_payto")?.run(::IbanPayTo), + selected_reserve_pub = it.getBytes("reserve_pub")?.run(::EddsaPublicKey), + ) + } + } + + suspend fun getStatus(uuid: UUID): BankWithdrawalOperationStatus? = db.conn { conn -> + val stmt = conn.prepareStatement(""" + SELECT + (amount).val as amount_val + ,(amount).frac as amount_frac + ,selection_done + ,aborted + ,confirmation_done + ,internal_payto_uri + FROM taler_withdrawal_operations + JOIN bank_accounts ON (wallet_bank_account=bank_account_id) + WHERE withdrawal_uuid=? + """) + stmt.setObject(1, uuid) + stmt.oneOrNull { + BankWithdrawalOperationStatus( + amount = TalerAmount( + it.getLong("amount_val"), + it.getInt("amount_frac"), + db.bankCurrency + ), + selection_done = it.getBoolean("selection_done"), + transfer_done = it.getBoolean("confirmation_done"), + aborted = it.getBoolean("aborted"), + sender_wire = it.getString("internal_payto_uri"), + confirm_transfer_url = null, + suggested_exchange = null ) } } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt @@ -114,22 +114,6 @@ fun ApplicationCall.longUriComponent(name: String): Long { } /** - * This handler factors out the checking of the query param - * and the retrieval of the related withdrawal database row. - * It throws 404 if the operation is not found, and throws 400 - * if the query param doesn't parse into a UUID. Currently - * used by the Taler Web/SPA and Integration API handlers. - */ -suspend fun ApplicationCall.getWithdrawal(db: Database, name: String): TalerWithdrawalOperation { - val opId = uuidUriComponent(name) - val op = db.withdrawal.get(opId) ?: throw notFound( - "Withdrawal operation $opId not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) - return op -} - -/** * This function creates the admin account ONLY IF it was * NOT found in the database. It sets it to a random password that * is only meant to be overridden by a dedicated CLI tool. diff --git a/bank/src/test/kotlin/AmountTest.kt b/bank/src/test/kotlin/AmountTest.kt @@ -48,7 +48,7 @@ class AmountTest { // Check bank transaction stmt.executeUpdate() - val txRes = db.bankTransaction( + val (txRes, _) = db.bankTransaction( creditAccountPayto = IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ"), debitAccountUsername = "merchant", subject = "test", diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -6,7 +6,6 @@ import io.ktor.http.* import io.ktor.http.content.* import io.ktor.server.engine.* import io.ktor.server.testing.* -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import net.taler.wallet.crypto.Base32Crockford @@ -105,7 +104,7 @@ class CoreBankTokenApiTest { } } }.run { - val never: TokenSuccessResponse = Json.decodeFromString(bodyAsText()) + val never: TokenSuccessResponse = json() assertEquals(Instant.MAX, never.expiration.t_s) } @@ -165,7 +164,7 @@ class CoreBankTokenApiTest { } } -class CoreBankAccountsMgmtApiTest { +class CoreBankAccountsApiTest { // Testing the account creation and its idempotency @Test fun createAccountTest() = bankSetup { _ -> @@ -309,21 +308,11 @@ class CoreBankAccountsMgmtApiTest { // fail to delete, due to a non-zero balance. - client.post("/accounts/exchange/transactions") { - basicAuth("exchange", "exchange-password") - jsonBody { - "payto_uri" to "payto://iban/MERCHANT-IBAN-XYZ?message=payout&amount=KUDOS:1" - } - }.assertNoContent() + tx("exchange", "KUDOS:1", "merchant") client.delete("/accounts/merchant") { basicAuth("admin", "admin-password") }.assertConflict(TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO) - client.post("/accounts/merchant/transactions") { - basicAuth("merchant", "merchant-password") - jsonBody { - "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout&amount=KUDOS:1" - } - }.assertNoContent() + tx("merchant", "KUDOS:1", "exchange") client.delete("/accounts/merchant") { basicAuth("admin", "admin-password") }.assertNoContent() @@ -395,7 +384,7 @@ class CoreBankAccountsMgmtApiTest { client.get("/accounts/merchant") { basicAuth("admin", "admin-password") }.assertOk().run { - val obj: AccountData = Json.decodeFromString(bodyAsText()) + val obj: AccountData = json() assertEquals("Another Foo", obj.name) assertEquals(cashout.canonical, obj.cashout_payto_uri?.canonical) assertEquals("+99", obj.contact_data?.phone) @@ -491,7 +480,7 @@ class CoreBankAccountsMgmtApiTest { } } // All accounts - client.get("/accounts"){ + client.get("/accounts?delta=10"){ basicAuth("admin", "admin-password") }.run { assertOk() @@ -523,7 +512,7 @@ class CoreBankAccountsMgmtApiTest { client.get("/accounts/merchant") { basicAuth("merchant", "merchant-password") }.assertOk().run { - val obj: AccountData = Json.decodeFromString(bodyAsText()) + val obj: AccountData = json() assertEquals("Merchant", obj.name) } @@ -574,30 +563,20 @@ class CoreBankTransactionsApiTest { } } - authRoutine("/accounts/merchant/transactions?delta=7", method = HttpMethod.Get) + authRoutine("/accounts/merchant/transactions", method = HttpMethod.Get) // Check error when no transactions - client.get("/accounts/merchant/transactions?delta=7") { + client.get("/accounts/merchant/transactions") { basicAuth("merchant", "merchant-password") }.assertNoContent() // Gen three transactions from merchant to exchange repeat(3) { - client.post("/accounts/merchant/transactions") { - basicAuth("merchant", "merchant-password") - jsonBody { - "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout$it&amount=KUDOS:0.$it" - } - }.assertNoContent() + tx("merchant", "KUDOS:0.$it", "exchange") } // Gen two transactions from exchange to merchant repeat(2) { - client.post("/accounts/exchange/transactions") { - basicAuth("exchange", "exchange-password") - jsonBody { - "payto_uri" to "payto://iban/MERCHANT-IBAN-XYZ?message=payout$it&amount=KUDOS:0.$it" - } - }.assertNoContent() + tx("exchange", "KUDOS:0.$it", "merchant") } // Check no useless polling @@ -630,22 +609,12 @@ class CoreBankTransactionsApiTest { } } delay(200) - client.post("/accounts/merchant/transactions") { - basicAuth("merchant", "merchant-password") - jsonBody { - "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout_poll&amount=KUDOS:4.2" - } - }.assertNoContent() + tx("merchant", "KUDOS:4.2", "exchange") } // Testing ranges. repeat(30) { - client.post("/accounts/merchant/transactions") { - basicAuth("merchant", "merchant-password") - jsonBody { - "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout_range&amount=KUDOS:0.001" - } - }.assertNoContent() + tx("merchant", "KUDOS:0.001", "exchange") } // forward range: @@ -665,19 +634,13 @@ class CoreBankTransactionsApiTest { authRoutine("/accounts/merchant/transactions/1", method = HttpMethod.Get) // Create transaction - client.post("/accounts/merchant/transactions") { - basicAuth("merchant", "merchant-password") - jsonBody { - "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout" - "amount" to "KUDOS:0.3" - } - }.assertNoContent() + tx("merchant", "KUDOS:0.3", "exchange") // Check OK client.get("/accounts/merchant/transactions/1") { basicAuth("merchant", "merchant-password") }.assertOk().run { - val tx: BankAccountTransactionInfo = Json.decodeFromString(bodyAsText()) - assertEquals("payout", tx.subject) + val tx: BankAccountTransactionInfo = json() + assertEquals("tx", tx.subject) assertEquals(TalerAmount("KUDOS:0.3"), tx.amount) } // Check unknown transaction @@ -704,28 +667,34 @@ class CoreBankTransactionsApiTest { client.post("/accounts/merchant/transactions") { basicAuth("merchant", "merchant-password") jsonBody(valid_req) - }.assertNoContent() - client.get("/accounts/merchant/transactions/1") { - basicAuth("merchant", "merchant-password") }.assertOk().run { - val tx: BankAccountTransactionInfo = Json.decodeFromString(bodyAsText()) - assertEquals("payout", tx.subject) - assertEquals(TalerAmount("KUDOS:0.3"), tx.amount) + val id = json<TransactionCreateResponse>().row_id + client.get("/accounts/merchant/transactions/$id") { + basicAuth("merchant", "merchant-password") + }.assertOk().run { + val tx: BankAccountTransactionInfo = json() + assertEquals("payout", tx.subject) + assertEquals(TalerAmount("KUDOS:0.3"), tx.amount) + } } + // Check amount in payto_uri client.post("/accounts/merchant/transactions") { basicAuth("merchant", "merchant-password") jsonBody { "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout2&amount=KUDOS:1.05" } - }.assertNoContent() - client.get("/accounts/merchant/transactions/3") { - basicAuth("merchant", "merchant-password") }.assertOk().run { - val tx: BankAccountTransactionInfo = Json.decodeFromString(bodyAsText()) - assertEquals("payout2", tx.subject) - assertEquals(TalerAmount("KUDOS:1.05"), tx.amount) + val id = json<TransactionCreateResponse>().row_id + client.get("/accounts/merchant/transactions/$id") { + basicAuth("merchant", "merchant-password") + }.assertOk().run { + val tx: BankAccountTransactionInfo = json() + assertEquals("payout2", tx.subject) + assertEquals(TalerAmount("KUDOS:1.05"), tx.amount) + } } + // Check amount in payto_uri precedence client.post("/accounts/merchant/transactions") { basicAuth("merchant", "merchant-password") @@ -733,13 +702,15 @@ class CoreBankTransactionsApiTest { "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout3&amount=KUDOS:1.05" "amount" to "KUDOS:10.003" } - }.assertNoContent() - client.get("/accounts/merchant/transactions/5") { - basicAuth("merchant", "merchant-password") }.assertOk().run { - val tx: BankAccountTransactionInfo = Json.decodeFromString(bodyAsText()) - assertEquals("payout3", tx.subject) - assertEquals(TalerAmount("KUDOS:1.05"), tx.amount) + val id = json<TransactionCreateResponse>().row_id + client.get("/accounts/merchant/transactions/$id") { + basicAuth("merchant", "merchant-password") + }.assertOk().run { + val tx: BankAccountTransactionInfo = json() + assertEquals("payout3", tx.subject) + assertEquals(TalerAmount("KUDOS:1.05"), tx.amount) + } } // Testing the wrong currency client.post("/accounts/merchant/transactions") { @@ -790,7 +761,7 @@ class CoreBankTransactionsApiTest { client.get("/accounts/merchant") { basicAuth("admin", "admin-password") }.assertOk().run { - val obj: AccountData = Json.decodeFromString(bodyAsText()) + val obj: AccountData = json() assertEquals( if (merchantDebt) CreditDebitInfo.debit else CreditDebitInfo.credit, obj.balance.credit_debit_indicator) @@ -799,7 +770,7 @@ class CoreBankTransactionsApiTest { client.get("/accounts/customer") { basicAuth("admin", "admin-password") }.assertOk().run { - val obj: AccountData = Json.decodeFromString(bodyAsText()) + val obj: AccountData = json() assertEquals( if (customerDebt) CreditDebitInfo.debit else CreditDebitInfo.credit, obj.balance.credit_debit_indicator) @@ -816,7 +787,7 @@ class CoreBankTransactionsApiTest { jsonBody { "payto_uri" to "payto://iban/CUSTOMER-IBAN-XYZ?message=payout2&amount=KUDOS:3" } - }.assertNoContent() + }.assertOk() } client.post("/accounts/merchant/transactions") { basicAuth("merchant", "merchant-password") @@ -831,7 +802,7 @@ class CoreBankTransactionsApiTest { jsonBody { "payto_uri" to "payto://iban/MERCHANT-IBAN-XYZ?message=payout2&amount=KUDOS:10" } - }.assertNoContent() + }.assertOk() checkBalance(false, "KUDOS:1.6", true, "KUDOS:4") } } @@ -1017,13 +988,7 @@ class CoreBankWithdrawalApiTest { }.assertOk() // Send too much money - client.post("/accounts/merchant/transactions") { - basicAuth("merchant", "merchant-password") - jsonBody { - "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout&amount=KUDOS:5" - } - }.assertNoContent() - + tx("merchant", "KUDOS:5", "exchange") client.post("/withdrawals/$uuid/confirm") .assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) @@ -1325,13 +1290,7 @@ class CoreBankCashoutApiTest { }.assertOk().run { val id = json<CashoutPending>().cashout_id // Send too much money - client.post("/accounts/customer/transactions") { - basicAuth("customer", "customer-password") - jsonBody { - "payto_uri" to "payto://iban/merchant-IBAN-XYZ?message=payout&amount=KUDOS:9" - } - }.assertNoContent() - + tx("customer", "KUDOS:9", "merchant") client.post("/accounts/customer/cashouts/$id/confirm"){ basicAuth("customer", "customer-password") jsonBody { "tan" to smsCode("+99") } diff --git a/bank/src/test/kotlin/SecurityTest.kt b/bank/src/test/kotlin/SecurityTest.kt @@ -41,7 +41,7 @@ class SecurityTest { client.post("/accounts/merchant/transactions") { basicAuth("merchant", "merchant-password") jsonBody(valid_req) - }.assertNoContent() + }.assertOk() // Check body too big client.post("/accounts/merchant/transactions") { diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt b/bank/src/test/kotlin/WireGatewayApiTest.kt @@ -25,7 +25,7 @@ class WireGatewayApiTest { amount = TalerAmount("KUDOS:10"), timestamp = Instant.now(), ).run { - assertEquals(BankTransactionResult.SUCCESS, this) + assertEquals(BankTransactionResult.SUCCESS, first) } } diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt @@ -103,6 +103,17 @@ suspend fun ApplicationTestBuilder.assertBalance(account: String, info: CreditDe } } +suspend fun ApplicationTestBuilder.tx(from: String, amount: String, to: String): Long { + return client.post("/accounts/$from/transactions") { + basicAuth("$from", "$from-password") + jsonBody { + "payto_uri" to "payto://iban/$to-IBAN-XYZ?message=tx&amount=$amount" + } + }.assertOk().run { + json<TransactionCreateResponse>().row_id + } +} + suspend fun ApplicationTestBuilder.transfer(amount: String) { client.post("/accounts/exchange/taler-wire-gateway/transfer") { basicAuth("exchange", "exchange-password")