libeufin

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

commit 642d36b0b738311dd0e12212f664a237bd64138c
parent 7e6bed1a0cc09950198a84fe5977b8ed8f40091b
Author: MS <ms@taler.net>
Date:   Fri, 29 Sep 2023 14:23:17 +0200

Checking big amounts.

Checking that debt limits are honored and that gigantic
amounts fail soon at parsing time.

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt | 28+++++++++++++++++++---------
Mbank/src/main/kotlin/tech/libeufin/bank/Database.kt | 7+++----
Mbank/src/test/kotlin/LibeuFinApiTest.kt | 44++++++++++++++++++++++++++++++++++++++++++++
Mbank/src/test/kotlin/TalerApiTest.kt | 25+++++++++++++++----------
Mdatabase-versioning/procedures.sql | 1+
5 files changed, 82 insertions(+), 23 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt @@ -240,6 +240,8 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { val accountName = call.expectUriComponent("USERNAME") if (c.login != accountName) throw unauthorized("User ${c.login} not allowed to withdraw for account '${accountName}'") val req = call.receive<BankAccountCreateWithdrawalRequest>() // Checking that the user has enough funds. + if(req.amount.currency != ctx.currency) + throw badRequest("Wrong currency: ${req.amount.currency}") val b = db.bankAccountGetFromOwnerId(c.expectRowId()) ?: throw internalServerError("Customer '${c.login}' lacks bank account.") if (!isBalanceEnough( @@ -375,26 +377,34 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { // Creates a bank transaction. post("/accounts/{USERNAME}/transactions") { - val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?: throw unauthorized() + val c: Customer = call.authenticateBankRequest(db, TokenScope.readwrite) ?: throw unauthorized() val resourceName = call.expectUriComponent("USERNAME") // admin has no rights here. if ((c.login != resourceName) && (call.getAuthToken() == null)) throw forbidden() val txData = call.receive<BankAccountTransactionCreate>() val payto = parsePayto(txData.payto_uri) ?: throw badRequest("Invalid creditor Payto") val paytoWithoutParams = stripIbanPayto(txData.payto_uri) val subject = payto.message ?: throw badRequest("Wire transfer lacks subject") - val debtorId = c.dbRowId - ?: throw internalServerError("Debtor database ID not found") // This performs already a SELECT on the bank account, like the wire transfer will do as well later! + val debtorBankAccount = db.bankAccountGetFromOwnerId(c.expectRowId()) + ?: throw internalServerError("Debtor bank account not found") + if (txData.amount.currency != ctx.currency) throw badRequest( + "Wrong currency: ${txData.amount.currency}", + talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH + ) + if (!isBalanceEnough( + balance = debtorBankAccount.expectBalance(), + due = txData.amount, + hasBalanceDebt = debtorBankAccount.hasDebt, + maxDebt = debtorBankAccount.maxDebt + )) + throw conflict(hint = "Insufficient balance.", talerEc = TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT) logger.info("creditor payto: $paytoWithoutParams") - val creditorCustomerData = db.bankAccountGetFromInternalPayto(paytoWithoutParams) ?: throw notFound( + val creditorBankAccount = db.bankAccountGetFromInternalPayto(paytoWithoutParams) ?: throw notFound( "Creditor account not found", TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT ) - if (txData.amount.currency != ctx.currency) throw badRequest( - "Wrong currency: ${txData.amount.currency}", talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH - ) val dbInstructions = BankInternalTransaction( - debtorAccountId = debtorId, - creditorAccountId = creditorCustomerData.owningCustomerId, + debtorAccountId = debtorBankAccount.expectRowId(), + creditorAccountId = creditorBankAccount.expectRowId(), subject = subject, amount = txData.amount, transactionDate = Instant.now() diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt @@ -21,6 +21,7 @@ package tech.libeufin.bank import org.postgresql.jdbc.PgConnection +import org.postgresql.util.PSQLException import org.slf4j.Logger import org.slf4j.LoggerFactory import tech.libeufin.util.getJdbcConnectionFromPg @@ -176,10 +177,8 @@ class Database(private val dbConfig: String, private val bankCurrency: String) { stmt.execute() } catch (e: SQLException) { logger.error(e.message) - // NOTE: it seems that _every_ error gets the 0 code. - if (e.errorCode == 0) return false - // rethrowing, not to hide other types of errors. - throw e + if (e.sqlState == "23505") return false // unique_violation + throw e // rethrowing, not to hide other types of errors. } return true } diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt b/bank/src/test/kotlin/LibeuFinApiTest.kt @@ -130,6 +130,50 @@ class LibeuFinApiTest { } val obj: BankAccountTransactionInfo = Json.decodeFromString(r.bodyAsText()) assert(obj.subject == "payout") + // Testing the wrong currency. + val wrongCurrencyResp = client.post("/accounts/foo/transactions") { + expectSuccess = false + basicAuth("foo", "pw") + contentType(ContentType.Application.Json) + // expectSuccess = true + setBody( + """{ + "payto_uri": "payto://iban/AC${barId}?message=payout", + "amount": "EUR:3.3" + } + """.trimIndent() + ) + } + assert(wrongCurrencyResp.status == HttpStatusCode.BadRequest) + // Surpassing the debt limit. + val unallowedDebtResp = client.post("/accounts/foo/transactions") { + expectSuccess = false + basicAuth("foo", "pw") + contentType(ContentType.Application.Json) + // expectSuccess = true + setBody( + """{ + "payto_uri": "payto://iban/AC${barId}?message=payout", + "amount": "KUDOS:555" + } + """.trimIndent() + ) + } + assert(unallowedDebtResp.status == HttpStatusCode.Conflict) + val bigAmount = client.post("/accounts/foo/transactions") { + expectSuccess = false + basicAuth("foo", "pw") + contentType(ContentType.Application.Json) + // expectSuccess = true + setBody( + """{ + "payto_uri": "payto://iban/AC${barId}?message=payout", + "amount": "KUDOS:${"5".repeat(200)}" + } + """.trimIndent() + ) + } + assert(bigAmount.status == HttpStatusCode.BadRequest) } } diff --git a/bank/src/test/kotlin/TalerApiTest.kt b/bank/src/test/kotlin/TalerApiTest.kt @@ -53,11 +53,6 @@ class TalerApiTest { assert(db.bankAccountCreate(bankAccountFoo) != null) assert(db.customerCreate(customerBar) != null) assert(db.bankAccountCreate(bankAccountBar) != null) - // Give the exchange reasonable debt allowance: - assert(db.bankAccountSetMaxDebt( - 1L, - TalerAmount(1000, 0, "KUDOS") - )) // Do POST /transfer. testApplication { application { @@ -68,17 +63,29 @@ class TalerApiTest { "request_uid": "entropic 0", "wtid": "entropic 1", "exchange_base_url": "http://exchange.example.com/", - "amount": "KUDOS:33", + "amount": "KUDOS:55", "credit_account": "BAR-IBAN-ABC" } """.trimIndent() + // Checking exchange debt constraint. + val resp = client.post("/accounts/foo/taler-wire-gateway/transfer") { + basicAuth("foo", "pw") + contentType(ContentType.Application.Json) + expectSuccess = false + setBody(req) + } + assert(resp.status == HttpStatusCode.Conflict) + // Giving debt allowance and checking the OK case. + assert(db.bankAccountSetMaxDebt( + 1L, + TalerAmount(1000, 0, "KUDOS") + )) client.post("/accounts/foo/taler-wire-gateway/transfer") { basicAuth("foo", "pw") contentType(ContentType.Application.Json) expectSuccess = true setBody(req) } - // println(resp.bodyAsText()) // check idempotency client.post("/accounts/foo/taler-wire-gateway/transfer") { basicAuth("foo", "pw") @@ -86,7 +93,6 @@ class TalerApiTest { expectSuccess = true setBody(req) } - // println(idemResp.bodyAsText()) // Trigger conflict due to reused request_uid val r = client.post("/accounts/foo/taler-wire-gateway/transfer") { basicAuth("foo", "pw") @@ -103,8 +109,7 @@ class TalerApiTest { """.trimIndent()) } assert(r.status == HttpStatusCode.Conflict) - /* Triggering currency mismatch. This mainly tests - * the TalerAmount "@Contextual" parser. */ + // Triggering currency mismatch val currencyMismatchResp = client.post("/accounts/foo/taler-wire-gateway/transfer") { basicAuth("foo", "pw") contentType(ContentType.Application.Json) diff --git a/database-versioning/procedures.sql b/database-versioning/procedures.sql @@ -146,6 +146,7 @@ SELECT IF (maybe_balance_insufficient) THEN out_exchange_balance_insufficient=TRUE; + RETURN; END IF; out_exchange_balance_insufficient=FALSE; INSERT