libeufin

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

commit fccc2eff22a0528658d6d1a42ac27e5dd2b4f34c
parent 9d8599f68e063c47eba1a9b1348f79f6357023b2
Author: Antoine A <>
Date:   Fri, 19 Apr 2024 07:50:29 +0900

Improve EBICS testbench

Diffstat:
Mnexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt | 2+-
Mnexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt | 26++++++++++++++++----------
Mnexus/src/main/kotlin/tech/libeufin/nexus/KeyFiles.kt | 4++--
Mnexus/src/test/kotlin/helpers.kt | 6+++---
Mtestbench/README.md | 16++++++++++++++--
Mtestbench/build.gradle | 1+
Mtestbench/src/main/kotlin/Main.kt | 42++++++++++++++++++++++++++++++++++++------
7 files changed, 73 insertions(+), 24 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt @@ -174,7 +174,7 @@ class EbicsSubmit : CliktCommand("Submits any initiated payment found in the dat try { submitBatch(ctx, db) } catch (e: Exception) { - throw Exception("Failed to submit payments") + throw Exception("Failed to submit payments", e) } // TODO take submitBatch taken time in the delay delay(frequency.toKotlinDuration()) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt @@ -277,12 +277,12 @@ sealed interface TxNotification { /** ISO20022 incoming payment */ data class IncomingPayment( + /** ISO20022 AccountServicerReference */ + val bankId: String, val amount: TalerAmount, val wireTransferSubject: String, - val debitPaytoUri: String, override val executionTime: Instant, - /** ISO20022 AccountServicerReference */ - val bankId: String + val debitPaytoUri: String ): TxNotification { override fun toString(): String { return "IN ${executionTime.fmtDate()} $amount '$bankId' debitor=$debitPaytoUri subject=\"$wireTransferSubject\"" @@ -291,12 +291,12 @@ data class IncomingPayment( /** ISO20022 outgoing payment */ data class OutgoingPayment( - val amount: TalerAmount, - override val executionTime: Instant, /** ISO20022 MessageIdentification */ val messageId: String, + val amount: TalerAmount, + val wireTransferSubject: String? = null, // not showing in camt.054 + override val executionTime: Instant, val creditPaytoUri: String? = null, // not showing in camt.054 - val wireTransferSubject: String? = null // not showing in camt.054 ): TxNotification { override fun toString(): String { return "OUT ${executionTime.fmtDate()} $amount '$messageId' creditor=$creditPaytoUri subject=\"$wireTransferSubject\"" @@ -349,6 +349,7 @@ fun parseTx( var msgId = opt("Refs")?.opt("MsgId")?.text() val subject = opt("RmtInf")?.map("Ustrd") { text() }?.joinToString("") var debtorPayto = opt("RltdPties") { payto("Dbtr") } + var creditorPayto = opt("RltdPties") { payto("Cdtr") } RawTx( kind, bookDate, @@ -358,7 +359,8 @@ fun parseTx( ref, msgId, subject, - debtorPayto + debtorPayto, + creditorPayto ) } } @@ -388,6 +390,7 @@ fun parseTx( var msgId = opt("Refs")?.opt("MsgId")?.text() val subject = opt("RmtInf")?.map("Ustrd") { text() }?.joinToString("") var debtorPayto = opt("RltdPties") { payto("Dbtr") } + var creditorPayto = opt("RltdPties") { payto("Cdtr") } RawTx( kind, bookDate, @@ -397,7 +400,8 @@ fun parseTx( ref, msgId, subject, - debtorPayto + debtorPayto, + creditorPayto ) } } @@ -440,7 +444,8 @@ private data class RawTx( val ref: String?, val msgId: String?, val subject: String?, - val debtorPayto: String? + val debtorPayto: String?, + val creditorPayto: String? ) private class TxErr(val msg: String): Exception(msg) @@ -478,7 +483,8 @@ private fun parseTxLogic(raw: RawTx): TxNotification { OutgoingPayment( amount = raw.amount, messageId = raw.msgId, - executionTime = raw.bookDate + executionTime = raw.bookDate, + creditPaytoUri = raw.creditorPayto ) } else -> throw Exception("Unknown transaction notification kind '${raw.kind}'") diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/KeyFiles.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/KeyFiles.kt @@ -118,7 +118,7 @@ fun generateNewKeys(): ClientPrivateKeysFile = submitted_ini = false ) -private inline fun <reified T> persistJsonFile(obj: T, path: Path, name: String) { +inline fun <reified T> persistJsonFile(obj: T, path: Path, name: String) { val content = try { JSON.encodeToString(obj) } catch (e: Exception) { @@ -158,7 +158,7 @@ fun persistBankKeys(keys: BankPublicKeysFile, location: Path) = persistJsonFile( fun persistClientKeys(keys: ClientPrivateKeysFile, location: Path) = persistJsonFile(keys, location, "client private keys") -private inline fun <reified T> loadJsonFile(path: Path, name: String): T? { +inline fun <reified T> loadJsonFile(path: Path, name: String): T? { val content = try { path.readText() } catch (e: Exception) { diff --git a/nexus/src/test/kotlin/helpers.kt b/nexus/src/test/kotlin/helpers.kt @@ -68,7 +68,7 @@ fun serverSetup( } } -val grothoffPayto = "payto://iban/CH6208704048981247126?receiver-name=Grothoff%20Hans" +val grothoffPayto = "payto://iban/CH4189144589712575493?receiver-name=Grothoff%20Hans" val clientKeys = generateNewKeys() @@ -91,7 +91,7 @@ fun genInitPay( ) = InitiatedPayment( id = -1, amount = TalerAmount(44, 0, "KUDOS"), - creditPaytoUri = "payto://iban/CH9300762011623852957?receiver-name=Test", + creditPaytoUri = "payto://iban/CH4189144589712575493?receiver-name=Test", wireTransferSubject = subject, initiationTime = Instant.now(), requestUid = requestUid @@ -111,7 +111,7 @@ fun genInPay(subject: String) = fun genOutPay(subject: String, messageId: String) = OutgoingPayment( amount = TalerAmount(44, 0, "KUDOS"), - creditPaytoUri = "payto://iban/CH9300762011623852957?receiver-name=Test", + creditPaytoUri = "payto://iban/CH4189144589712575493?receiver-name=Test", wireTransferSubject = subject, executionTime = Instant.now(), messageId = messageId diff --git a/testbench/README.md b/testbench/README.md @@ -5,6 +5,7 @@ To add a platform write a minimal configuration file at `testbench/test/PLATFORM/ebics.conf` such as : ``` ini +# testbench/test/PLATFORM/ebics.conf [nexus-ebics] currency = CHF @@ -30,4 +31,16 @@ make testbench platform=PLATFORM If HOST_BASE_URL is one a known test platform we will generate and then offer to reset client private keys to test keys registration, otherwise, we will expect existing keys to be found at `testbench/test/PLATFORM/client-ebics-keys.json`. -This minimal configuration will be augmented on start, you can find the full documentation at `testbench/test/PLATFORM/ebics.edited.conf`. -\ No newline at end of file +This minimal configuration will be augmented on start, you can find the full documentation at `testbench/test/PLATFORM/ebics.edited.conf`. + +By default, the testbench will use a random dummy IBAN when issuing transactions, but you can specify a real IBAN for real-life testing in the testbench configuration at `testbench/test/config.json` : + +``` json +// testbench/test/PLATFORM/ebics.conf +{ + "payto": { + "CHF": "payto://iban/CH4189144589712575493?receiver-name=John%20Smith", + "EUR": "payto://iban/DE54500105177452372744?receiver-name=John%20Smith" + } +} +``` diff --git a/testbench/build.gradle b/testbench/build.gradle @@ -1,6 +1,7 @@ plugins { id("kotlin") id("application") + id("org.jetbrains.kotlin.plugin.serialization") version "$kotlin_version" } java { diff --git a/testbench/src/main/kotlin/Main.kt b/testbench/src/main/kotlin/Main.kt @@ -23,13 +23,17 @@ import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.ProgramResult import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.testing.test +import io.ktor.http.URLBuilder +import io.ktor.http.takeFrom import io.ktor.client.* import io.ktor.client.engine.cio.* import kotlinx.coroutines.runBlocking +import kotlinx.serialization.Serializable import tech.libeufin.nexus.LibeufinNexusCommand import tech.libeufin.nexus.loadBankKeys import tech.libeufin.nexus.loadClientKeys import tech.libeufin.nexus.loadConfig +import tech.libeufin.nexus.loadJsonFile import kotlin.io.path.* val nexusCmd = LibeufinNexusCommand() @@ -64,6 +68,11 @@ data class Kind(val name: String, val settings: String?) { val test get() = settings != null } +@Serializable +data class Config( + val payto: Map<String, String> +) + class Cli : CliktCommand("Run integration tests on banks provider") { val platform by argument() @@ -105,6 +114,10 @@ class Cli : CliktCommand("Run integration tests on banks provider") { else -> Kind("Unknown", null) } + // Read testbench config + val benchCfg: Config = loadJsonFile(Path("test/config.json"), "testbench config") + ?: Config(emptyMap()) + // Prepare cmds val log = "DEBUG" val flags = " -c $conf -L $log" @@ -113,11 +126,15 @@ class Cli : CliktCommand("Run integration tests on banks provider") { val bankKeysPath = cfg.requirePath("nexus-ebics", "bank_public_keys_file") val currency = cfg.requireString("nexus-ebics", "currency") - val payto = when (currency) { - "CHF" -> "payto://iban/CH6208704048981247126?receiver-name=Grothoff%20Hans" - "EUR" -> "payto://iban/GENODEM1GLS/DE76430609674126675300?receiver-name=Grothoff%20Hans" - else -> throw Exception("Missing test payto for $currency") - } + val dummyPaytos = mapOf( + "CHF" to "payto://iban/CH4189144589712575493?receiver-name=John%20Smith", + "EUR" to "payto://iban/DE54500105177452372744?receiver-name=John%20Smith" + ) + val dummyPayto = dummyPaytos[currency] + ?: throw Exception("Missing dummy payto for $currency") + val payto = benchCfg.payto[currency] ?: dummyPayto + ?: throw Exception("Missing test payto for $currency") + val recoverDoc = when (cfg.requireString("nexus-ebics", "bank_dialect")) { "gls" -> "statement" else -> "notification" @@ -175,12 +192,25 @@ class Cli : CliktCommand("Run integration tests on banks provider") { } else { put("tx", suspend { step("Submit new transaction") - // TODO interactive payment editor nexusCmd.run("initiate-payment $flags \"$payto&amount=$currency:1.1&message=single%20transaction%20test\"") nexusCmd.run("ebics-submit $ebicsFlags") Unit }) } + put("tx-bad-name", suspend { + val badPayto = URLBuilder().takeFrom(payto) + badPayto.parameters.set("receiver-name", "John Smith") + step("Submit new transaction with a bad name") + nexusCmd.run("initiate-payment $flags \"$badPayto&amount=$currency:1.1&message=This%20should%20fail%20because%20bad%20name\"") + nexusCmd.run("ebics-submit $ebicsFlags") + Unit + }) + put("tx-dummy", suspend { + step("Submit new transaction to a dummy IBAN") + nexusCmd.run("initiate-payment $flags \"$dummyPayto&amount=$currency:1.1&message=This%20should%20fail%20because%20dummy\"") + nexusCmd.run("ebics-submit $ebicsFlags") + Unit + }) } while (true) { var clientKeys = loadClientKeys(clientKeysPath)