libeufin

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

commit 4b8314699995083a48515b578ebed5da828c477f
parent 4990150b3aac8066c0cdee4c7dc9170e433e73c9
Author: MS <ms@taler.net>
Date:   Tue,  4 Apr 2023 21:07:58 +0200

Introducing account-less Access API's endpoints.

Diffstat:
Mnexus/src/test/kotlin/SandboxAccessApiTest.kt | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 144+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
2 files changed, 158 insertions(+), 58 deletions(-)

diff --git a/nexus/src/test/kotlin/SandboxAccessApiTest.kt b/nexus/src/test/kotlin/SandboxAccessApiTest.kt @@ -5,15 +5,87 @@ import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.testing.* +import io.ktor.util.* import kotlinx.coroutines.runBlocking import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.transactions.transaction import org.junit.Test +import tech.libeufin.nexus.bankaccount.getBankAccount import tech.libeufin.sandbox.* class SandboxAccessApiTest { val mapper = ObjectMapper() + /** + * Testing that ..access-api/withdrawals/{wopid} and + * ..access-api/accounts/{account_name}/withdrawals/{wopid} + * are handled in the same way. + */ + @Test + fun doubleUriStyle() { + // Creating one withdrawal operation. + withTestDatabase { + prepSandboxDb() + val wo: TalerWithdrawalEntity = transaction { + TalerWithdrawalEntity.new { + this.amount = "TESTKUDOS:3.3" + walletBankAccount = getBankAccountFromLabel("foo") + selectedExchangePayto = "payto://iban/SANDBOXX/${BAR_USER_IBAN}" + reservePub = "not used" + selectionDone = true + } + } + testApplication { + application(sandboxApp) + // Showing withdrawal info. + val get_with_account = client.get("/demobanks/default/access-api/accounts/foo/withdrawals/${wo.wopid}") { + expectSuccess = true + } + val get_without_account = client.get("/demobanks/default/access-api/withdrawals/${wo.wopid}") { + expectSuccess = true + } + assert(get_without_account.bodyAsText() == get_with_account.bodyAsText()) + assert(get_with_account.bodyAsText() == get_without_account.bodyAsText()) + // Confirming a withdrawal. + val confirm_with_account = client.post("/demobanks/default/access-api/accounts/foo/withdrawals/${wo.wopid}/confirm") { + expectSuccess = true + } + val confirm_without_account = client.post("/demobanks/default/access-api/withdrawals/${wo.wopid}/confirm") { + expectSuccess = true + } + assert(confirm_with_account.status.value == confirm_without_account.status.value) + assert(confirm_with_account.bodyAsText() == confirm_without_account.bodyAsText()) + // Aborting one withdrawal. + var wo_to_abort = transaction { + TalerWithdrawalEntity.new { + this.amount = "TESTKUDOS:3.3" + walletBankAccount = getBankAccountFromLabel("foo") + selectedExchangePayto = "payto://iban/SANDBOXX/${BAR_USER_IBAN}" + reservePub = "not used" + selectionDone = true + } + } + val abort_with_account = client.post("/demobanks/default/access-api/accounts/foo/withdrawals/${wo_to_abort.wopid}/abort") { + expectSuccess = true + } + wo_to_abort = transaction { + TalerWithdrawalEntity.new { + this.amount = "TESTKUDOS:3.3" + walletBankAccount = getBankAccountFromLabel("foo") + selectedExchangePayto = "payto://iban/SANDBOXX/${BAR_USER_IBAN}" + reservePub = "not used" + selectionDone = true + } + } + val abort_without_account = client.post("/demobanks/default/access-api/withdrawals/${wo_to_abort.wopid}/abort") { + expectSuccess = true + } + assert(abort_with_account.status.value == abort_without_account.status.value) + // Not checking the content as they abort two different operations. + } + } + } + // Move funds between accounts. @Test fun wireTransfer() { diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -487,6 +487,73 @@ fun setJsonHandler(ctx: ObjectMapper) { ) } +private suspend fun getWithdrawal(call: ApplicationCall) { + val op = getWithdrawalOperation(call.expectUriComponent("withdrawal_id")) + if (!op.selectionDone && op.reservePub != null) throw internalServerError( + "Unselected withdrawal has a reserve public key", + LibeufinErrorCode.LIBEUFIN_EC_INCONSISTENT_STATE + ) + call.respond(object { + val amount = op.amount + val aborted = op.aborted + val confirmation_done = op.confirmationDone + val selection_done = op.selectionDone + val selected_reserve_pub = op.reservePub + val selected_exchange_account = op.selectedExchangePayto + }) +} + +private suspend fun confirmWithdrawal(call: ApplicationCall) { + val withdrawalId = call.expectUriComponent("withdrawal_id") + transaction { + val wo = getWithdrawalOperation(withdrawalId) + if (wo.aborted) throw SandboxError( + HttpStatusCode.Conflict, + "Cannot confirm an aborted withdrawal." + ) + if (!wo.selectionDone) throw SandboxError( + HttpStatusCode.UnprocessableEntity, + "Cannot confirm a unselected withdrawal: " + + "specify exchange and reserve public key via Integration API first." + ) + /** + * The wallet chose not to select any exchange, use the default. + */ + val demobank = ensureDemobank(call) + if (wo.selectedExchangePayto == null) { + wo.selectedExchangePayto = demobank.config.suggestedExchangePayto + } + val exchangeBankAccount = getBankAccountFromPayto( + wo.selectedExchangePayto ?: throw internalServerError( + "Cannot withdraw without an exchange." + ) + ) + if (!wo.confirmationDone) { + wireTransfer( + debitAccount = wo.walletBankAccount, + creditAccount = exchangeBankAccount, + amount = wo.amount, + subject = wo.reservePub ?: throw internalServerError( + "Cannot transfer funds without reserve public key." + ), + // provide the currency. + demobank = ensureDemobank(call) + ) + wo.confirmationDone = true + } + wo.confirmationDone + } + call.respond(object {}) +} + +private suspend fun abortWithdrawal(call: ApplicationCall) { + val withdrawalId = call.expectUriComponent("withdrawal_id") + val operation = getWithdrawalOperation(withdrawalId) + if (operation.confirmationDone) throw conflict("Cannot abort paid withdrawal.") + transaction { operation.aborted = true } + call.respond(object {}) +} + val sandboxApp: Application.() -> Unit = { install(CallLogging) { this.level = Level.DEBUG @@ -1283,19 +1350,12 @@ val sandboxApp: Application.() -> Unit = { } // Information about one withdrawal. get("/accounts/{account_name}/withdrawals/{withdrawal_id}") { - val op = getWithdrawalOperation(call.expectUriComponent("withdrawal_id")) - if (!op.selectionDone && op.reservePub != null) throw internalServerError( - "Unselected withdrawal has a reserve public key", - LibeufinErrorCode.LIBEUFIN_EC_INCONSISTENT_STATE - ) - call.respond(object { - val amount = op.amount - val aborted = op.aborted - val confirmation_done = op.confirmationDone - val selection_done = op.selectionDone - val selected_reserve_pub = op.reservePub - val selected_exchange_account = op.selectedExchangePayto - }) + getWithdrawal(call) + return@get + } + // account-less style: + get("/withdrawals/{withdrawal_id}") { + getWithdrawal(call) return@get } // Create a new withdrawal operation. @@ -1372,54 +1432,22 @@ val sandboxApp: Application.() -> Unit = { } // Confirm a withdrawal: no basic auth, because the ID should be unguessable. post("/accounts/{account_name}/withdrawals/{withdrawal_id}/confirm") { - val withdrawalId = call.expectUriComponent("withdrawal_id") - transaction { - val wo = getWithdrawalOperation(withdrawalId) - if (wo.aborted) throw SandboxError( - HttpStatusCode.Conflict, - "Cannot confirm an aborted withdrawal." - ) - if (!wo.selectionDone) throw SandboxError( - HttpStatusCode.UnprocessableEntity, - "Cannot confirm a unselected withdrawal: " + - "specify exchange and reserve public key via Integration API first." - ) - /** - * The wallet chose not to select any exchange, use the default. - */ - val demobank = ensureDemobank(call) - if (wo.selectedExchangePayto == null) { - wo.selectedExchangePayto = demobank.config.suggestedExchangePayto - } - val exchangeBankAccount = getBankAccountFromPayto( - wo.selectedExchangePayto ?: throw internalServerError( - "Cannot withdraw without an exchange." - ) - ) - if (!wo.confirmationDone) { - wireTransfer( - debitAccount = wo.walletBankAccount, - creditAccount = exchangeBankAccount, - amount = wo.amount, - subject = wo.reservePub ?: throw internalServerError( - "Cannot transfer funds without reserve public key." - ), - // provide the currency. - demobank = ensureDemobank(call) - ) - wo.confirmationDone = true - } - wo.confirmationDone - } - call.respond(object {}) + confirmWithdrawal(call) return@post } + // account-less style: + post("/withdrawals/{withdrawal_id}/confirm") { + confirmWithdrawal(call) + return@post + } + // Aborting withdrawals: post("/accounts/{account_name}/withdrawals/{withdrawal_id}/abort") { - val withdrawalId = call.expectUriComponent("withdrawal_id") - val operation = getWithdrawalOperation(withdrawalId) - if (operation.confirmationDone) throw conflict("Cannot abort paid withdrawal.") - transaction { operation.aborted = true } - call.respond(object {}) + abortWithdrawal(call) + return@post + } + // account-less style: + post("/withdrawals/{withdrawal_id}/abort") { + abortWithdrawal(call) return@post } // Bank account basic information.