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:
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