libeufin

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

commit bc5c9cfae023ee45a586bd1f74ab9c4615d25b7e
parent a41440e41dc6649670beb9c40e6e5d214df3e803
Author: Antoine A <>
Date:   Mon,  5 Feb 2024 18:44:59 +0100

Add nexus commands to trigger outgoing payments and generate fake incoming payments

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/Main.kt | 6+++---
Dcontrib/payment_initiation_debug.sh | 51---------------------------------------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mtestbench/src/main/kotlin/Main.kt | 37+++++++------------------------------
Mtestbench/src/test/kotlin/IntegrationTest.kt | 12++----------
5 files changed, 130 insertions(+), 96 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -1,6 +1,6 @@ /* * This file is part of LibEuFin. - * Copyright (C) 2023 Taler Systems S.A. + * Copyright (C) 2023-2024 Taler Systems S.A. * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -432,8 +432,8 @@ class CreateAccountOption: OptionGroup() { help = "Payto URI of this account" ).convert { Payto.parse(it) } val debit_threshold: TalerAmount? by option( - help = "Max debit allowed for this account") - .convert { TalerAmount(it) } + help = "Max debit allowed for this account" + ).convert { TalerAmount(it) } } class CreateAccount : CliktCommand( diff --git a/contrib/payment_initiation_debug.sh b/contrib/payment_initiation_debug.sh @@ -1,51 +0,0 @@ -#!/bin/bash - -# This script injects an initiated payment into its -# table, in order to test the ebics-submit subcommand. - -usage() { - echo "Usage: ./payment_initiation.sh CONFIG_FILE IBAN_CREDITOR PAYMENT_SUBJECT" - echo - echo "Pays a fixed amount of 1 CHF to the IBAN_CREDITOR with the PAYMENT_SUBJECT." - echo "It requires the EBICS keying to be already carried out, see ebics-setup" - echo "subcommand at libeufin-nexus(1)" -} - -# Detecting the help case. -if test "$1" = "--help" -o "$1" = "-h" -o -z ${1:-}; - then usage - exit -fi - -set -eu - -CONFIG_FILE=$1 -IBAN_CREDITOR=$2 -PAYMENT_SUBJECT=$3 - -# Getting the database connection. -DB_NAME=$(taler-config -c $1 -s nexus-postgres -o config) -echo database: $DB_NAME - -# Optionally reading the user-provided request UID. -if test -n "${LIBEUFIN_SUBMIT_REQUEST_UID:-}" - then SUBMIT_REQUEST_UID="$LIBEUFIN_SUBMIT_REQUEST_UID" - else SUBMIT_REQUEST_UID=$(uuidgen | cut -c -30) -fi - -# Finally inserting the initiated payment into the database. -INSERT_COMMAND=" -INSERT INTO libeufin_nexus.initiated_outgoing_transactions - (amount, - wire_transfer_subject, - initiation_time, - credit_payto_uri, - request_uid) - VALUES ((1,0), - '${PAYMENT_SUBJECT}', - $(($(date +%s) * 1000000)), - 'payto://iban/POFICHBE/${IBAN_CREDITOR}?receiver-name=Merchant', - '${SUBMIT_REQUEST_UID}')" - -# Only logging errors. -psql $DB_NAME -c "$INSERT_COMMAND" > /dev/null diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -1,6 +1,7 @@ /* * This file is part of LibEuFin. * Copyright (C) 2023 Stanisci and Dold. + * Copyright (C) 2024 Taler Systems S.A. * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -26,7 +27,10 @@ package tech.libeufin.nexus import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.subcommands -import com.github.ajalt.clikt.parameters.options.versionOption +import com.github.ajalt.clikt.parameters.options.* +import com.github.ajalt.clikt.parameters.groups.* +import com.github.ajalt.clikt.parameters.arguments.* +import kotlinx.coroutines.* import io.ktor.client.* import io.ktor.util.* import org.slf4j.Logger @@ -34,6 +38,7 @@ import org.slf4j.LoggerFactory import tech.libeufin.nexus.ebics.* import tech.libeufin.common.* import java.nio.file.Path +import java.time.Instant val NEXUS_CONFIG_SOURCE = ConfigSource("libeufin", "libeufin-nexus", "libeufin-nexus") val logger: Logger = LoggerFactory.getLogger("libeufin-nexus") @@ -190,6 +195,7 @@ class EbicsSetupConfig(val config: TalerConfig) { } } + /** * Abstracts the config loading * @@ -207,13 +213,123 @@ fun TalerConfig.dbConfig(): DatabaseConfig = sqlDir = requirePath("libeufin-nexusdb-postgres", "sql_dir") ) +class InitiatePayment: CliktCommand("Initiate an outgoing payment") { + private val common by CommonOption() + private val amount by option( + "--amount", + help = "The amount to transfer, payto 'amount' parameter takes the precedence" + ).convert { TalerAmount(it) } + private val subject by option( + "--subject", + help = "The payment subject, payto 'message' parameter takes the precedence" + ) + private val requestUid by option( + "--request-uid", + help = "The payment request UID" + ) + private val payto by argument( + help = "The credited account IBAN payto URI" + ).convert { Payto.parse(it).expectIban() } + + override fun run() = cliCmd(logger, common.log) { + val cfg = loadConfig(common.config) + val dbCfg = cfg.dbConfig() + val currency = cfg.requireString("nexus-ebics", "currency") + + val subject = payto.message ?: subject ?: throw Exception("Missing subject") + val amount = payto.amount ?: amount ?: throw Exception("Missing amount") + + if (payto.receiverName == null) + throw Exception("Missing receiver name in creditor payto") + + if (amount.currency != currency) + throw Exception("Wrong currency: expected $currency got ${amount.currency}") + + val requestUid = requestUid ?: run { + val bytes = ByteArray(16) + kotlin.random.Random.nextBytes(bytes) + Base32Crockford.encode(bytes) + } + + Database(dbCfg.dbConnStr).use { db -> + runBlocking { + db.initiatedPaymentCreate( + InitiatedPayment( + id = -1, + amount = amount, + wireTransferSubject = subject, + creditPaytoUri = payto.toString(), + initiationTime = Instant.now(), + requestUid = requestUid + ) + ) + } + } + } +} + +class FakeIncoming: CliktCommand("Genere a fake incoming payment") { + private val common by CommonOption() + private val amount by option( + "--amount", + help = "The amount to transfer, payto 'amount' parameter takes the precedence" + ).convert { TalerAmount(it) } + private val subject by option( + "--subject", + help = "The payment subject, payto 'message' parameter takes the precedence" + ) + private val payto by argument( + help = "The debited account IBAN payto URI" + ).convert { Payto.parse(it).expectIban() } + + override fun run() = cliCmd(logger, common.log) { + val cfg = loadConfig(common.config) + val dbCfg = cfg.dbConfig() + val currency = cfg.requireString("nexus-ebics", "currency") + + val subject = payto.message ?: subject ?: throw Exception("Missing subject") + val amount = payto.amount ?: amount ?: throw Exception("Missing amount") + + if (amount.currency != currency) + throw Exception("Wrong currency: expected $currency got ${amount.currency}") + + val bankId = run { + val bytes = ByteArray(16) + kotlin.random.Random.nextBytes(bytes) + Base32Crockford.encode(bytes) + } + + Database(dbCfg.dbConnStr).use { db -> + runBlocking { + ingestIncomingPayment(db, + IncomingPayment( + amount = amount, + debitPaytoUri = payto.toString(), + wireTransferSubject = subject, + executionTime = Instant.now(), + bankId = bankId + ) + ) + } + } + } +} + +class TestingCmd : CliktCommand("Testing helper commands", name = "testing") { + init { + subcommands(FakeIncoming()) + } + + override fun run() = Unit +} + /** * Main CLI class that collects all the subcommands. */ class LibeufinNexusCommand : CliktCommand() { init { versionOption(getVersion()) - subcommands(EbicsSetup(), DbInit(), EbicsSubmit(), EbicsFetch(), CliConfigCmd(NEXUS_CONFIG_SOURCE)) + subcommands(EbicsSetup(), DbInit(), EbicsSubmit(), EbicsFetch(), InitiatePayment(), CliConfigCmd(NEXUS_CONFIG_SOURCE), TestingCmd()) } override fun run() = Unit } diff --git a/testbench/src/main/kotlin/Main.kt b/testbench/src/main/kotlin/Main.kt @@ -1,6 +1,6 @@ /* * This file is part of LibEuFin. - * Copyright (C) 2023 Taler Systems S.A. + * Copyright (C) 2023-2024 Taler Systems S.A. * * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -19,7 +19,6 @@ package tech.libeufin.testbench -import tech.libeufin.nexus.Database as NexusDb import tech.libeufin.nexus.* import tech.libeufin.bank.* import tech.libeufin.common.* @@ -120,11 +119,10 @@ class Cli : CliktCommand("Run integration tests on banks provider") { val payto = "payto://iban/CH6208704048981247126?receiver-name=Grothoff%20Hans" runBlocking { - step("Test init ${kind.name}") + step("Init ${kind.name}") if (ask("Reset DB ? y/n>") == "y") nexusCmd.test("dbinit -r $flags").assertOk() else nexusCmd.test("dbinit $flags").assertOk() - val nexusDb = NexusDb("postgresql:///libeufincheck") val cmds = buildMap<String, suspend () -> Unit> { put("reset-db", suspend { @@ -157,28 +155,14 @@ class Cli : CliktCommand("Run integration tests on banks provider") { Unit }) put("tx", suspend { - step("Test submit one transaction") - nexusDb.initiatedPaymentCreate(InitiatedPayment( - id = -1, - amount = TalerAmount("CFH:42"), - creditPaytoUri = payto, - wireTransferSubject = "single transaction test", - initiationTime = Instant.now(), - requestUid = Base32Crockford.encode(randBytes(16)) - )) + step("Submit one transaction") + nexusCmd.test("initiate-payment $flags \"$payto&amount=CHF:42&message=single%20transaction%20test\"").assertOk() nexusCmd.test("ebics-submit $ebicsFlags").assertOk() }) put("txs", suspend { - step("Test submit many transaction") + step("Submit many transaction") repeat(4) { - nexusDb.initiatedPaymentCreate(InitiatedPayment( - id = -1, - amount = TalerAmount("CFH:${100L+it}"), - creditPaytoUri = payto, - wireTransferSubject = "multi transaction test $it", - initiationTime = Instant.now(), - requestUid = Base32Crockford.encode(randBytes(16)) - )) + nexusCmd.test("initiate-payment $flags --amount=CHF:${100L+it} --subject \"multi transaction test $it\" \"$payto\"").assertOk() } nexusCmd.test("ebics-submit $ebicsFlags").assertOk() }) @@ -186,14 +170,7 @@ class Cli : CliktCommand("Run integration tests on banks provider") { put("tx", suspend { step("Submit new transaction") // TODO interactive payment editor - nexusDb.initiatedPaymentCreate(InitiatedPayment( - id = -1, - amount = TalerAmount("CFH:1.1"), - creditPaytoUri = payto, - wireTransferSubject = "single transaction test", - initiationTime = Instant.now(), - requestUid = Base32Crockford.encode(randBytes(16)) - )) + nexusCmd.test("initiate-payment $flags \"$payto&amount=CHF:1.1&message=single%20transaction%20test\"").assertOk() nexusCmd.test("ebics-submit $ebicsFlags").assertOk() }) } diff --git a/testbench/src/test/kotlin/IntegrationTest.kt b/testbench/src/test/kotlin/IntegrationTest.kt @@ -1,6 +1,6 @@ /* * This file is part of LibEuFin. - * Copyright (C) 2023 Taler Systems S.A. + * Copyright (C) 2023-2024 Taler Systems S.A. * * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -281,15 +281,7 @@ class IntegrationTest { val reservePub = randBytes(32); val amount = TalerAmount("EUR:${20+i}") val subject = "cashin test $i: ${Base32Crockford.encode(reservePub)}" - ingestIncomingPayment(db, - IncomingPayment( - amount = amount, - debitPaytoUri = userPayTo.toString(), - wireTransferSubject = subject, - executionTime = Instant.now(), - bankId = Base32Crockford.encode(reservePub) - ) - ) + nexusCmd.run("testing fake-incoming $flags --subject \"$subject\" --amount $amount $userPayTo") val converted = client.get("http://0.0.0.0:8080/conversion-info/cashin-rate?amount_debit=EUR:${20 + i}") .assertOkJson<ConversionResponse>().amount_credit client.get("http://0.0.0.0:8080/accounts/exchange/transactions") {