libeufin

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

commit d727095c1bdffc9700ca657bc8bd88af44929949
parent 1877afa8413fed57f4cbe2c63920a1104c596898
Author: Antoine A <>
Date:   Mon, 14 Oct 2024 15:03:12 +0200

bank: check withdrawal ownership on abort in core bank API

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/api/BankIntegrationApi.kt | 2+-
Mbank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt | 2+-
Mbank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt | 8++++++--
Mbank/src/test/kotlin/CoreBankApiTest.kt | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbank/src/test/kotlin/bench.kt | 2+-
Mdatabase-versioning/libeufin-bank-procedures.sql | 5+++++
6 files changed, 78 insertions(+), 5 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/BankIntegrationApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/BankIntegrationApi.kt @@ -119,7 +119,7 @@ fun Routing.bankIntegrationApi(db: Database, ctx: BankConfig) { } post("/taler-integration/withdrawal-operation/{wopid}/abort") { val uuid = call.uuidPath("wopid") - when (db.withdrawal.abort(uuid)) { + when (db.withdrawal.abort(uuid, null)) { AbortResult.UnknownOperation -> throw notFound( "Withdrawal operation '$uuid' not found", TalerErrorCode.BANK_TRANSACTION_NOT_FOUND diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt @@ -607,7 +607,7 @@ private fun Routing.coreBankWithdrawalApi(db: Database, ctx: BankConfig) { } post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/abort") { val opId = call.uuidPath("withdrawal_id") - when (db.withdrawal.abort(opId)) { + when (db.withdrawal.abort(opId, call.username)) { AbortResult.UnknownOperation -> throw notFound( "Withdrawal operation $opId not found", TalerErrorCode.BANK_TRANSACTION_NOT_FOUND diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt @@ -100,15 +100,19 @@ class WithdrawalDAO(private val db: Database) { } /** Abort withdrawal operation [uuid] */ - suspend fun abort(uuid: UUID): AbortResult = db.serializable( + suspend fun abort( + uuid: UUID, + username: String?, + ): AbortResult = db.serializable( """ SELECT out_no_op, out_already_confirmed - FROM abort_taler_withdrawal(?) + FROM abort_taler_withdrawal(?, ?) """ ) { setObject(1, uuid) + setString(2, username) one { when { it.getBoolean("out_no_op") -> AbortResult.UnknownOperation diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -1397,6 +1397,70 @@ class CoreBankWithdrawalApiTest { .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) } + // POST /accounts/USERNAME/withdrawals/withdrawal_id/abort + @Test + fun abort() = bankSetup { + authRoutine(HttpMethod.Post, "/accounts/merchant/withdrawals/42/abort") + + // Check abort created + client.postA("/accounts/merchant/withdrawals") { + json { "amount" to "KUDOS:1" } + }.assertOkJson<BankAccountCreateWithdrawalResponse> { + val uuid = it.withdrawal_id + + // Check OK + client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent() + // Check idempotence + client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent() + } + + // Check abort selected + client.postA("/accounts/merchant/withdrawals") { + json { "amount" to "KUDOS:1" } + }.assertOkJson<BankAccountCreateWithdrawalResponse> { + val uuid = it.withdrawal_id + withdrawalSelect(uuid) + + // Check OK + client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent() + // Check idempotence + client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent() + } + + // Check abort confirmed + client.postA("/accounts/merchant/withdrawals") { + json { "amount" to "KUDOS:1" } + }.assertOkJson<BankAccountCreateWithdrawalResponse> { + val uuid = it.withdrawal_id + withdrawalSelect(uuid) + client.postA("/accounts/merchant/withdrawals/$uuid/confirm").assertNoContent() + + // Check error + client.postA("/accounts/merchant/withdrawals/$uuid/abort") + .assertConflict(TalerErrorCode.BANK_ABORT_CONFIRM_CONFLICT) + } + + // Check confirm another user's operation + client.postA("/accounts/customer/withdrawals") { + json { "amount" to "KUDOS:1" } + }.assertOkJson<BankAccountCreateWithdrawalResponse> { + val uuid = it.withdrawal_id + withdrawalSelect(uuid) + + // Check error + client.postA("/accounts/merchant/withdrawals/$uuid/abort") + .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + } + + // Check bad UUID + client.postA("/accounts/merchant/withdrawals/chocolate/abort") + .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) + + // Check unknown + client.postA("/accounts/merchant/withdrawals/${UUID.randomUUID()}/abort") + .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + } + // POST /accounts/USERNAME/withdrawals/withdrawal_id/confirm @Test fun confirm() = bankSetup { diff --git a/bank/src/test/kotlin/bench.kt b/bank/src/test/kotlin/bench.kt @@ -257,7 +257,7 @@ class Bench { "amount" to "KUDOS:0.0001" } }.assertOkJson<BankAccountCreateWithdrawalResponse>().withdrawal_id - client.post("/taler-integration/withdrawal-operation/$uuid/abort") + client.postA("/accounts/customer/withdrawals/$uuid/abort") .assertNoContent() } diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql @@ -1048,6 +1048,7 @@ COMMENT ON FUNCTION select_taler_withdrawal IS 'Set details of a withdrawal oper CREATE FUNCTION abort_taler_withdrawal( IN in_withdrawal_uuid uuid, + IN in_username TEXT, OUT out_no_op BOOLEAN, OUT out_already_confirmed BOOLEAN ) @@ -1055,7 +1056,11 @@ LANGUAGE plpgsql AS $$ BEGIN UPDATE taler_withdrawal_operations SET aborted = NOT confirmation_done + FROM bank_accounts + JOIN customers ON owning_customer_id=customer_id WHERE withdrawal_uuid=in_withdrawal_uuid + AND wallet_bank_account=bank_account_id + AND (in_username IS NULL OR username = in_username) RETURNING confirmation_done INTO out_already_confirmed; IF NOT FOUND OR out_already_confirmed THEN