libeufin

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

commit edee80cc2d3363c68ea7dc7c588545924d0eb522
parent bc35aba3fbac892029ea17ee7ee4d1b4949650c0
Author: Antoine A <>
Date:   Wed, 30 Oct 2024 11:06:18 +0100

nexus: add export test cmd and update ktor

Diffstat:
Mbuild.gradle | 2+-
Mnexus/src/main/kotlin/tech/libeufin/nexus/Config.kt | 14+++++++++-----
Mnexus/src/main/kotlin/tech/libeufin/nexus/PDF.kt | 6+++---
Mnexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsSetup.kt | 2+-
Mnexus/src/main/kotlin/tech/libeufin/nexus/cli/Testing.kt | 35+++++++++++++++++++++++++++++++----
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsAdministrative.kt | 2+-
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsBTS.kt | 18+++++++++---------
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt | 10+++++-----
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsKeyMng.kt | 10+++++-----
Mtestbench/src/main/kotlin/Main.kt | 36+++++++++++++++++++++---------------
10 files changed, 86 insertions(+), 49 deletions(-)

diff --git a/build.gradle b/build.gradle @@ -20,7 +20,7 @@ if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)){ allprojects { ext { set("kotlin_version", "2.0.21") - set("ktor_version", "3.0.0") + set("ktor_version", "3.0.1") set("clikt_version", "5.0.1") set("coroutines_version", "1.9.0") set("postgres_version", "42.7.4") diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt @@ -53,17 +53,21 @@ class NexusSubmitConfig(config: TalerConfig) { val frequencyRaw = section.string("frequency").require() } -class NexusEbicsConfig( - sect: TalerConfigSection, -) { +class NexusHostConfig(sect: TalerConfigSection) { /** The bank base URL */ - val hostBaseUrl = sect.string("host_base_url").require() + val baseUrl = sect.string("host_base_url").require() /** The bank EBICS host ID */ val ebicsHostId = sect.string("host_id").require() /** EBICS user ID */ val ebicsUserId = sect.string("user_id").require() /** EBICS partner ID */ val ebicsPartnerId = sect.string("partner_id").require() +} + +class NexusEbicsConfig( + sect: TalerConfigSection, +) { + val host by lazy { NexusHostConfig(sect) } /** Bank account metadata */ val account = IbanAccountMetadata( iban = sect.string("iban").require(), @@ -89,7 +93,7 @@ class ApiConfig(section: TalerConfigSection) { } /** Configuration for libeufin-nexus */ -class NexusConfig internal constructor (private val cfg: TalerConfig) { +class NexusConfig internal constructor (val cfg: TalerConfig) { private val sect = cfg.section("nexus-ebics") val dbCfg by lazy { cfg.dbConfig() } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/PDF.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/PDF.kt @@ -62,9 +62,9 @@ fun generateKeysPdf( Paragraph( """ Datum: $dateStr - Host-ID: ${cfg.ebicsHostId} - User-ID: ${cfg.ebicsUserId} - Partner-ID: ${cfg.ebicsPartnerId} + Host-ID: ${cfg.host.ebicsHostId} + User-ID: ${cfg.host.ebicsUserId} + Partner-ID: ${cfg.host.ebicsPartnerId} ES version: A006 """.trimIndent() ) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsSetup.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsSetup.kt @@ -222,7 +222,7 @@ class EbicsSetup: CliktCommand() { bankKeys ).download(EbicsOrder.V3.HKD, null, null) { stream -> val (partner, users) = EbicsAdministrative.parseHKD(stream) - val user = users.find { it -> it.id == cfg.ebics.ebicsUserId } + val user = users.find { it -> it.id == cfg.ebics.host.ebicsUserId } // Debug logging logger.debug { buildString { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/Testing.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/Testing.kt @@ -22,19 +22,20 @@ package tech.libeufin.nexus.cli import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.Context import com.github.ajalt.clikt.core.subcommands -import com.github.ajalt.clikt.parameters.arguments.argument -import com.github.ajalt.clikt.parameters.arguments.convert +import com.github.ajalt.clikt.parameters.arguments.* import com.github.ajalt.clikt.parameters.groups.provideDelegate import com.github.ajalt.clikt.parameters.options.convert import com.github.ajalt.clikt.parameters.options.default import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option -import com.github.ajalt.clikt.parameters.types.enum +import com.github.ajalt.clikt.parameters.types.* import tech.libeufin.common.* import tech.libeufin.nexus.* import tech.libeufin.nexus.ebics.* import tech.libeufin.nexus.iso20022.* +import java.util.zip.* import java.time.Instant +import java.io.* class Wss: CliktCommand() { override fun help(context: Context) = "Listen to EBICS instant notification over websocket" @@ -273,9 +274,35 @@ class ListCmd: CliktCommand("list") { } } +class ExportCmt: CliktCommand("export") { + override fun help(context: Context) = "Export pending batches as pain001 messages" + + private val common by CommonOption() + private val out by argument().file() + + override fun run() = cliCmd(logger, common.log) { + nexusConfig(common.config).withDb { db, cfg -> + ZipOutputStream(BufferedOutputStream(FileOutputStream(out))).use { zip -> + db.initiated.batch(Instant.now(), randEbicsId()) + val ebicsCfg = cfg.ebics + db.initiated.submittable(cfg.currency).forEach { batch -> + val entry = ZipEntry("${batch.creationDate}-${batch.messageId}.xml") + zip.putNextEntry(entry) + val msg = batchToPain001Msg(ebicsCfg.account, batch) + val xml = createPain001( + msg = msg, + dialect = ebicsCfg.dialect + ) + zip.write(xml) + } + } + } + } +} + class TestingCmd : CliktCommand("testing") { init { - subcommands(FakeIncoming(), ListCmd(), EbicsDownload(), TxCheck(), Wss()) + subcommands(FakeIncoming(), ListCmd(), EbicsDownload(), TxCheck(), Wss(), ExportCmt()) } override fun help(context: Context) = "Testing helper commands" diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsAdministrative.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsAdministrative.kt @@ -71,7 +71,7 @@ object EbicsAdministrative { fun HEV(cfg: NexusEbicsConfig): ByteArray { return XmlBuilder.toBytes("ebicsHEVRequest") { attr("xmlns", "http://www.ebics.org/H000") - el("HostID", cfg.ebicsHostId) + el("HostID", cfg.host.ebicsHostId) } } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsBTS.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsBTS.kt @@ -50,11 +50,11 @@ class EbicsBTS( el("header") { attr("authenticate", "true") el("static") { - el("HostID", cfg.ebicsHostId) + el("HostID", cfg.host.ebicsHostId) el("Nonce", nonce.encodeHex()) el("Timestamp", Instant.now().xmlDateTime()) - el("PartnerID", cfg.ebicsPartnerId) - el("UserID", cfg.ebicsUserId) + el("PartnerID", cfg.host.ebicsPartnerId) + el("UserID", cfg.host.ebicsUserId) // SystemID // Product el("OrderDetails") { @@ -107,7 +107,7 @@ class EbicsBTS( el("header") { attr("authenticate", "true") el("static") { - el("HostID", cfg.ebicsHostId) + el("HostID", cfg.host.ebicsHostId) el("TransactionID", transactionId) } el("mutable") { @@ -131,7 +131,7 @@ class EbicsBTS( el("header") { attr("authenticate", "true") el("static") { - el("HostID", cfg.ebicsHostId) + el("HostID", cfg.host.ebicsHostId) el("TransactionID", transactionId) } el("mutable") { @@ -154,11 +154,11 @@ class EbicsBTS( el("header") { attr("authenticate", "true") el("static") { - el("HostID", cfg.ebicsHostId) + el("HostID", cfg.host.ebicsHostId) el("Nonce", nonce.encodeUpHex()) el("Timestamp", Instant.now().xmlDateTime()) - el("PartnerID", cfg.ebicsPartnerId) - el("UserID", cfg.ebicsUserId) + el("PartnerID", cfg.host.ebicsPartnerId) + el("UserID", cfg.host.ebicsUserId) // SystemID // Product el("OrderDetails") { @@ -215,7 +215,7 @@ class EbicsBTS( el("header") { attr("authenticate", "true") el("static") { - el("HostID", cfg.ebicsHostId) + el("HostID", cfg.host.ebicsHostId) el("TransactionID", transactionId) } el("mutable") { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt @@ -96,7 +96,7 @@ suspend fun EbicsBTS.postBTS( phase: String, stepLogger: StepLogger? = null ): EbicsResponse<BTSResponse> { - val doc = client.postToBank(cfg.hostBaseUrl, xmlReq, phase, stepLogger) + val doc = client.postToBank(cfg.host.baseUrl, xmlReq, phase, stepLogger) if (!XMLUtil.verifyEbicsDocument( doc, bankKeys.bank_authentication_public_key @@ -281,7 +281,7 @@ suspend fun HEV( logger.info("Doing administrative request HEV") val txLog = ebicsLogger.tx("HEV") val req = EbicsAdministrative.HEV(cfg) - val xml = client.postToBank(cfg.hostBaseUrl, req, "HEV", txLog.step()) + val xml = client.postToBank(cfg.host.baseUrl, req, "HEV", txLog.step()) return EbicsAdministrative.parseHEV(xml).okOrFail("HEV") } @@ -300,7 +300,7 @@ suspend fun keyManagement( Dialect.postfinance -> true } val req = EbicsKeyMng(cfg, privs, ebics3).request(order) - val xml = client.postToBank(cfg.hostBaseUrl, req, order.name, txLog.step()) + val xml = client.postToBank(cfg.host.baseUrl, req, order.name, txLog.step()) return EbicsKeyMng.parseResponse(xml, privs.encryption_private_key) } @@ -327,8 +327,8 @@ fun prepareUploadPayload( payloadDigest, clientKeys.signature_private_key, ).encodeBase64()) - el("PartnerID", cfg.ebicsPartnerId) - el("UserID", cfg.ebicsUserId) + el("PartnerID", cfg.host.ebicsPartnerId) + el("UserID", cfg.host.ebicsUserId) } } // Generate ephemeral transaction key diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsKeyMng.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsKeyMng.kt @@ -79,13 +79,13 @@ class EbicsKeyMng( el("header") { attr("authenticate", "true") el("static") { - el("HostID", cfg.ebicsHostId) + el("HostID", cfg.host.ebicsHostId) if (order == HPB) { el("Nonce", getNonce(128).encodeUpHex()) el("Timestamp", Instant.now().xmlDateTime()) } - el("PartnerID", cfg.ebicsPartnerId) - el("UserID", cfg.ebicsUserId) + el("PartnerID", cfg.host.ebicsPartnerId) + el("UserID", cfg.host.ebicsUserId) el("OrderDetails") { if (ebics3) { el("AdminOrderType", order.name) @@ -128,8 +128,8 @@ class EbicsKeyMng( attr("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#") attr("xmlns", schema) build() - el("PartnerID", cfg.ebicsPartnerId) - el("UserID", cfg.ebicsUserId) + el("PartnerID", cfg.host.ebicsPartnerId) + el("UserID", cfg.host.ebicsUserId) }.inputStream().deflate().encodeBase64() } diff --git a/testbench/src/main/kotlin/Main.kt b/testbench/src/main/kotlin/Main.kt @@ -106,7 +106,8 @@ class Cli : CliktCommand() { val cfg = nexusConfig(conf) // Check if platform is known - val kind = when (cfg.ebics.hostBaseUrl) { + val host = cfg.cfg.section("nexus-ebics").string("host_base_url").orNull() + val kind = when (host) { "https://isotest.postfinance.ch/ebicsweb/ebicsweb" -> Kind("PostFinance IsoTest", "https://isotest.postfinance.ch/corporates/user/settings/ebics") "https://iso20022test.credit-suisse.com/ebicsweb/ebicsweb" -> @@ -172,6 +173,7 @@ class Cli : CliktCommand() { put("list-initiated", "List initiated payments", "testing list $flags initiated") put("wss", "Listen to notification over websocket", "testing wss $debugFlags") put("submit", "Submit pending transactions", "ebics-submit $ebicsFlags") + put("export", "Export pending batches as pain001 messages", "testing export $flags payments.zip") put("setup", "Setup", "ebics-setup $debugFlags") put("reset-keys", suspend { if (kind.test) { @@ -218,25 +220,29 @@ class Cli : CliktCommand() { put("tx-check", "Check transaction semantic", "testing tx-check $flags") } while (true) { - var clientKeys = loadClientKeys(clientKeysPath) - val bankKeys = loadBankKeys(bankKeysPath) - if (!kind.test && clientKeys == null) { - throw Exception("Clients keys are required to run netzbon tests") - } else if (clientKeys == null || !clientKeys.submitted_ini || !clientKeys.submitted_hia || bankKeys == null || !bankKeys.accepted) { - step("Run EBICS setup") - if (!nexusCmd.run("ebics-setup --auto-accept-keys $debugFlags")) { - clientKeys = loadClientKeys(clientKeysPath) - if (kind.test) { - if (clientKeys == null || !clientKeys.submitted_ini || !clientKeys.submitted_hia) { - msg("Got to ${kind.settings} and click on 'Reset EBICS user'") + // Automatic setup + if (host != null) { + var clientKeys = loadClientKeys(clientKeysPath) + val bankKeys = loadBankKeys(bankKeysPath) + if (!kind.test && clientKeys == null) { + throw Exception("Clients keys are required to run netzbon tests") + } else if (clientKeys == null || !clientKeys.submitted_ini || !clientKeys.submitted_hia || bankKeys == null || !bankKeys.accepted) { + step("Run EBICS setup") + if (!nexusCmd.run("ebics-setup --auto-accept-keys $debugFlags")) { + clientKeys = loadClientKeys(clientKeysPath) + if (kind.test) { + if (clientKeys == null || !clientKeys.submitted_ini || !clientKeys.submitted_hia) { + msg("Got to ${kind.settings} and click on 'Reset EBICS user'") + } else { + msg("Got to ${kind.settings} and click on 'Activate EBICS user'") + } } else { - msg("Got to ${kind.settings} and click on 'Activate EBICS user'") + msg("Activate your keys at your bank") } - } else { - msg("Activate your keys at your bank") } } } + // REPL val arg = ask("testbench> ")!!.trim() if (arg == "exit") break if (arg == "") continue