libeufin

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

commit c15f9dfa5a284dff23acf28d63d34d05a912afcd
parent afb4b8a9fdf93ae2cb0e01e8ba54fb9e9f66d573
Author: MS <ms@taler.net>
Date:   Tue, 10 Jan 2023 21:47:59 +0100

Introducing file based TAN channel.

With this feature, Sandbox writes the TAN to a
file under /tmp where it can be read by the tester
without setting up any SMS or e-mail channel.

Diffstat:
Mcli/bin/circuit_test.sh | 7++++---
Acli/bin/circuit_test_file_tan.sh | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcli/bin/libeufin-cli | 5++---
Msandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt | 27++++++++++++++++++++++-----
4 files changed, 154 insertions(+), 11 deletions(-)

diff --git a/cli/bin/circuit_test.sh b/cli/bin/circuit_test.sh @@ -21,6 +21,7 @@ echo -n Configure the default demobank... libeufin-sandbox config default echo DONE echo -n Start the bank... +export LIBEUFIN_SANDBOX_ADMIN_PASSWORD=circuit libeufin-sandbox serve &> sandbox.log & SANDBOX_PID=$! trap "echo -n 'killing the bank (pid $SANDBOX_PID)...'; kill $SANDBOX_PID; wait; echo DONE" EXIT @@ -33,7 +34,7 @@ curl http://localhost:5000/demobanks/default/circuit-api/config &> /dev/null echo DONE echo -n "Register new account..." export LIBEUFIN_SANDBOX_USERNAME=admin -export LIBEUFIN_SANDBOX_PASSWORD=secret +export LIBEUFIN_SANDBOX_PASSWORD=circuit export LIBEUFIN_NEW_CIRCUIT_ACCOUNT_PASSWORD=foo ./libeufin-cli \ sandbox --sandbox-url http://localhost:5000/ \ @@ -98,7 +99,7 @@ echo DONE # balance to zero. echo -n Bring the account to 0 balance... export LIBEUFIN_SANDBOX_USERNAME=admin -export LIBEUFIN_SANDBOX_PASSWORD=secret +export LIBEUFIN_SANDBOX_PASSWORD=circuit ./libeufin-cli \ sandbox --sandbox-url http://localhost:5000/ \ demobank \ @@ -109,7 +110,7 @@ export LIBEUFIN_SANDBOX_PASSWORD=secret echo DONE echo -n Delete the account... export LIBEUFIN_SANDBOX_USERNAME=admin -export LIBEUFIN_SANDBOX_PASSWORD=secret +export LIBEUFIN_SANDBOX_PASSWORD=circuit ./libeufin-cli \ sandbox --sandbox-url http://localhost:5000/ \ demobank \ diff --git a/cli/bin/circuit_test_file_tan.sh b/cli/bin/circuit_test_file_tan.sh @@ -0,0 +1,126 @@ +#!/bin/bash + +# Tests successful cases of the CLI acting +# as the client of the Circuit API. + +set -eu + +echo TESTING THE CLI SIDE OF THE CIRCUIT API +jq --version &> /dev/null || (echo "'jq' command not found"; exit 77) +curl --version &> /dev/null || (echo "'curl' command not found"; exit 77) + +DB_PATH=/tmp/circuit-test.sqlite3 +export LIBEUFIN_SANDBOX_DB_CONNECTION=jdbc:sqlite:$DB_PATH + +echo -n Delete previous data.. +rm -f $DB_PATH +echo DONE +echo -n Configure the default demobank... +libeufin-sandbox config default +echo DONE +echo -n Start the bank... +export LIBEUFIN_SANDBOX_ADMIN_PASSWORD=circuit +libeufin-sandbox serve &> sandbox.log & +SANDBOX_PID=$! +trap "echo -n 'killing the bank (pid $SANDBOX_PID)...'; kill $SANDBOX_PID; wait; echo DONE" EXIT +echo DONE +echo -n Wait for the bank... +curl --max-time 2 --retry-connrefused --retry-delay 1 --retry 10 http://localhost:5000/ &> /dev/null +echo DONE +echo Ask Circuit API /config... +curl http://localhost:5000/demobanks/default/circuit-api/config &> /dev/null +echo DONE +echo -n "Register new account..." +export LIBEUFIN_SANDBOX_USERNAME=admin +export LIBEUFIN_SANDBOX_PASSWORD=circuit +export LIBEUFIN_NEW_CIRCUIT_ACCOUNT_PASSWORD=foo +./libeufin-cli \ + sandbox --sandbox-url http://localhost:5000/ \ + demobank \ + circuit-register --name eee --username www \ + --cashout-address payto://iban/FIAT --internal-iban LOCAL +echo DONE +echo -n Reconfigure account specifying a phone number.. +# Give phone number. +export LIBEUFIN_SANDBOX_USERNAME=www +export LIBEUFIN_SANDBOX_PASSWORD=foo +./libeufin-cli \ + sandbox --sandbox-url http://localhost:5000/ \ + demobank \ + circuit-reconfig --cashout-address payto://iban/WWW --phone +999 +echo DONE +echo -n Create a cash-out operation... +CASHOUT_RESP=$(./libeufin-cli \ + sandbox --sandbox-url http://localhost:5000/ \ + demobank \ + circuit-cashout \ + --tan-channel=file \ + --amount-debit=EUR:1 \ + --amount-credit=CHF:0.95 +) +echo DONE +echo -n "Extract the cash-out UUID..." +CASHOUT_UUID=$(echo ${CASHOUT_RESP} | jq --raw-output '.uuid') +echo DONE +echo -n Get cash-out details... +RESP=$(./libeufin-cli \ + sandbox --sandbox-url http://localhost:5000/ \ + demobank \ + circuit-cashout-details \ + --uuid $CASHOUT_UUID +) +OPERATION_STATUS=$(echo $RESP | jq --raw-output '.status') +if ! test "$OPERATION_STATUS" = "PENDING"; then + echo Unexpected cash-out operation status found: $OPERATION_STATUS + exit 1 +fi +echo DONE +echo -n Abort the cash-out operation... +RESP=$(./libeufin-cli \ + sandbox --sandbox-url http://localhost:5000/ \ + demobank \ + circuit-cashout-abort \ + --uuid $CASHOUT_UUID +) +echo DONE +echo -n Create another cash-out operation... +CASHOUT_RESP=$(./libeufin-cli \ + sandbox --sandbox-url http://localhost:5000/ \ + demobank \ + circuit-cashout \ + --tan-channel=file \ + --amount-debit=EUR:1 \ + --amount-credit=CHF:0.95 +) +CASHOUT_UUID=$(echo ${CASHOUT_RESP} | jq --raw-output '.uuid') +echo DONE +echo Reading the TAN from /tmp/libeufin-cashout-tan.txt +INPUT_TAN=$(cat /tmp/libeufin-cashout-tan.txt) +echo -n Confirm the last cash-out operation... +./libeufin-cli \ + sandbox --sandbox-url http://localhost:5000/ \ + demobank \ + circuit-cashout-confirm --uuid $CASHOUT_UUID --tan $INPUT_TAN +echo DONE +# The user now has -1 balance. Let the bank +# award EUR:1 to them, in order to bring their +# balance to zero. +echo -n Bring the account to 0 balance... +export LIBEUFIN_SANDBOX_USERNAME=admin +export LIBEUFIN_SANDBOX_PASSWORD=circuit +./libeufin-cli \ + sandbox --sandbox-url http://localhost:5000/ \ + demobank \ + new-transaction \ + --bank-account admin \ + --payto-with-subject "payto://iban/SANDBOXX/LOCAL?message=bring-to-zero" \ + --amount EUR:1 +echo DONE +echo -n Delete the account... +export LIBEUFIN_SANDBOX_USERNAME=admin +export LIBEUFIN_SANDBOX_PASSWORD=circuit +./libeufin-cli \ + sandbox --sandbox-url http://localhost:5000/ \ + demobank \ + circuit-delete-account --username www +echo DONE diff --git a/cli/bin/libeufin-cli b/cli/bin/libeufin-cli @@ -1863,8 +1863,8 @@ def password_reconfig(obj, username): ) @click.option( "--tan-channel", - help="Indicates how to send the TAN to the user: only 'sms' or 'email' are valid values. If missing, the bank defaults to SMS", - required=False + help="Indicates how to send the TAN to the user: 'sms', 'email' and 'file' are valid values. If missing, the bank defaults to SMS. 'file' makes the server write the TAN to /tmp/libeufin-cashout-tan.txt, normally used for testing.", + required=False, ) @click.pass_obj def circuit_cashout(obj, subject, amount_debit, amount_credit, tan_channel): @@ -1878,7 +1878,6 @@ def circuit_cashout(obj, subject, amount_debit, amount_credit, tan_channel): req.update(subject=subject) if tan_channel: req.update(tan_channel=tan_channel) - cashout_creation_endpoint = obj.circuit_api_url("cashouts") try: resp = post( diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt @@ -8,6 +8,7 @@ import io.ktor.server.routing.* import org.jetbrains.exposed.sql.transactions.transaction import tech.libeufin.sandbox.CashoutOperationsTable.uuid import tech.libeufin.util.* +import java.io.File import java.math.BigDecimal import java.math.MathContext import java.util.* @@ -118,9 +119,17 @@ fun generateCashoutSubject( * NOTE: future versions take the supported TAN method from * the configuration, or options passed when starting the bank. */ -enum class SupportedTanChannels { SMS, EMAIL } -fun isTanChannelSupported(tanMethod: String): Boolean { - return listOf(SupportedTanChannels.SMS.name, SupportedTanChannels.EMAIL.name).contains(tanMethod.uppercase()) +const val LIBEUFIN_TAN_TMP_FILE = "/tmp/libeufin-cashout-tan.txt" +enum class SupportedTanChannels { + SMS, + EMAIL, + FILE // Test channel writing the TAN to the LIBEUFIN_TAN_TMP_FILE location. +} +fun isTanChannelSupported(tanChannel: String): Boolean { + enumValues<SupportedTanChannels>().forEach { + if (tanChannel.uppercase() == it.name) return true + } + return false } fun circuitApi(circuitRoute: Route) { @@ -243,12 +252,12 @@ fun circuitApi(circuitRoute: Route) { throw badRequest("The '${req::amount_debit.name}' field has the wrong currency") if (amountCredit.currency == demobank.currency) throw badRequest("The '${req::amount_credit.name}' field didn't change the currency.") - // check if TAN is supported. + // check if TAN is supported. Default to SMS, if that's missing. val tanChannel = req.tan_channel?.uppercase() ?: SupportedTanChannels.SMS.name if (!isTanChannelSupported(tanChannel)) throw SandboxError( HttpStatusCode.ServiceUnavailable, - "TAN method $tanChannel not supported." + "TAN channel '$tanChannel' not supported." ) // check if the user contact data would allow the TAN channel. val customer = getCustomer(username = user) @@ -299,6 +308,14 @@ fun circuitApi(circuitRoute: Route) { SupportedTanChannels.SMS.name -> { // TBD } + SupportedTanChannels.FILE.name -> { + try { + File(LIBEUFIN_TAN_TMP_FILE).writeText(op.tan) + } catch (e: Exception) { + logger.error(e.message) + throw internalServerError("File TAN failed: could not write to $LIBEUFIN_TAN_TMP_FILE") + } + } else -> throw internalServerError("The bank didn't catch a unsupported TAN channel: $tanChannel.") }