libeufin

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

Testing.kt (9027B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2024, 2025, 2026 Taler Systems S.A.
      4 
      5  * LibEuFin is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU Affero General Public License as
      7  * published by the Free Software Foundation; either version 3, or
      8  * (at your option) any later version.
      9 
     10  * LibEuFin is distributed in the hope that it will be useful, but
     11  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13  * Public License for more details.
     14 
     15  * You should have received a copy of the GNU Affero General Public
     16  * License along with LibEuFin; see the file COPYING.  If not, see
     17  * <http://www.gnu.org/licenses/>
     18  */
     19 
     20 package tech.libeufin.nexus.cli
     21 
     22 import com.github.ajalt.clikt.core.CliktCommand
     23 import com.github.ajalt.clikt.core.Context
     24 import com.github.ajalt.clikt.core.subcommands
     25 import com.github.ajalt.clikt.parameters.arguments.*
     26 import com.github.ajalt.clikt.parameters.groups.provideDelegate
     27 import com.github.ajalt.clikt.parameters.options.convert
     28 import com.github.ajalt.clikt.parameters.options.default
     29 import com.github.ajalt.clikt.parameters.options.flag
     30 import com.github.ajalt.clikt.parameters.options.option
     31 import com.github.ajalt.clikt.parameters.options.required
     32 import com.github.ajalt.clikt.parameters.types.*
     33 import com.github.ajalt.mordant.terminal.*
     34 import tech.libeufin.common.*
     35 import tech.libeufin.nexus.*
     36 import tech.libeufin.nexus.iso20022.*
     37 import tech.libeufin.ebics.*
     38 import tech.libeufin.ebics.test.txCheck
     39 import java.util.zip.*
     40 import java.time.Instant
     41 import java.io.*
     42 
     43 class Wss : TalerCmd() {
     44     override fun help(context: Context) = "Listen to EBICS instant notification over websocket"
     45 
     46     private val ebicsLog by ebicsLogOption()
     47 
     48     override fun run() = cliCmd(logger) {
     49         nexusConfig(config).withDb { db, cfg ->
     50             val (clientKeys, bankKeys) = expectFullKeys(cfg.ebics)
     51             val client = EbicsClient(
     52                 cfg.ebics.host,
     53                 httpClient(),
     54                 db.ebics,
     55                 EbicsLogger(ebicsLog),
     56                 clientKeys,
     57                 bankKeys
     58             )
     59             val wssNotifications = listenForNotification(client)
     60             if (wssNotifications != null) {
     61                 while (true) {
     62                     wssNotifications.receive()
     63                     logger.debug("{}", wssNotifications)
     64                 }
     65             }
     66         }
     67     }
     68 }
     69 
     70 class FakeIncoming : TalerCmd() {
     71     override fun help(context: Context) = "Genere a fake incoming payment"
     72 
     73     private val amount by option(
     74         "--amount",
     75         help = "The payment amount, credit-payto 'amount' parameter takes the precedence"
     76     ).convert { TalerAmount(it) }
     77     private val creditFee by option(
     78         "--credit-fee",
     79         help = "The payment credit fee"
     80     ).convert { TalerAmount(it) }
     81     private val subject by option(
     82         "--subject",
     83         help = "The payment subject, credit-payto 'message' parameter takes the precedence"
     84     )
     85     private val chQrr by option(
     86         "--ch-qrr",
     87         help = "The payment reference, credit-payto 'ch-qrr' parameter takes the precedence"
     88     )
     89     private val creditPayto by option(
     90         "--credit-payto",
     91         help = "The credited account IBAN payto URI"
     92     ).convert { Payto.parse(it).expectIban() }
     93     private val debitPayto by option(
     94         "--debit-payto",
     95         help = "The debited account IBAN payto URI"
     96     ).convert { Payto.parse(it).expectIban() }
     97     private val payto by argument(
     98         name = "deprecated",
     99         help = "deprecated"
    100     ).convert { Payto.parse(it).expectIban() }.optional()
    101 
    102     override fun run() = cliCmd(logger) {
    103         nexusConfig(config).withDb { db, cfg ->
    104             val amount = requireNotNull(creditPayto?.amount ?: payto?.amount ?: amount) { "Missing amount" }
    105             val subject = creditPayto?.message ?: payto?.message ?: subject
    106             val reference = creditPayto?.chQrr ?: payto?.chQrr ?: chQrr
    107 
    108             creditPayto?.let {
    109                 if (reference != null) {
    110                     require(it.iban == cfg.ebics.qrIban) {
    111                         "Creditor must be the exchange QRR account expected ${cfg.ebics.account.iban} got ${it.iban}"
    112                     }
    113                 } else {
    114                     require(it.iban == cfg.ebics.account.iban) {
    115                         "Creditor must be the exchange expected ${cfg.ebics.account.iban} got ${it.iban}"
    116                     }
    117                 }
    118             }
    119 
    120             require(amount.currency == cfg.currency) {
    121                 "Wrong currency: expected ${cfg.currency} got ${amount.currency}"
    122             }
    123             registerIncomingPayment(
    124                 db, cfg.ingest,
    125                 IncomingPayment(
    126                     amount = amount,
    127                     debtor = debitPayto ?: payto ?: IbanPayto.rand("Testing Account", Country.valueOf(cfg.ebics.account.iban.value.substring(0 until 2))),
    128                     subject = reference ?: subject ?: "",
    129                     creditFee = creditFee,
    130                     executionTime = Instant.now(),
    131                     id = IncomingId(null, randEbicsId(), null)
    132                 )
    133             )
    134         }
    135     }
    136 }
    137 
    138 class TxCheck : TalerCmd() {
    139     override fun help(context: Context) = "Check transaction semantic"
    140 
    141     override fun run() = cliCmd(logger) {
    142         val nexusCgf = nexusConfig(config)
    143         val cfg = nexusCgf.ebics
    144         val (clientKeys, bankKeys) = expectFullKeys(cfg)
    145         val order = cfg.dialect.downloadDoc(OrderDoc.acknowledgement)
    146         val client = httpClient()
    147         val result = txCheck(client, cfg.host, clientKeys, bankKeys, order[0], cfg.dialect.directDebit())
    148         println("$result")
    149     }
    150 }
    151 
    152 enum class ListKind {
    153     incoming,
    154     outgoing,
    155     initiated;
    156 
    157     fun description(): String = when (this) {
    158         incoming -> "Incoming transactions"
    159         outgoing -> "Outgoing transactions"
    160         initiated -> "Initiated transactions"
    161     }
    162 }
    163 
    164 class EbicsDownload : TalerCmd("ebics-btd") {
    165     override fun help(context: Context) = "Perform EBICS requests"
    166 
    167     private val type by option().default("BTD")
    168     private val name by option()
    169     private val scope by option()
    170     private val messageName by option()
    171     private val messageVersion by option()
    172     private val container by option()
    173     private val option by option()
    174     private val ebicsLog by ebicsLogOption()
    175     private val pinnedStart by option(
    176         help = "Constant YYYY-MM-DD date for the earliest document" +
    177                 " to download (only consumed in --transient mode).  The" +
    178                 " latest document is always until the current time."
    179     )
    180     private val peek by option(
    181         "--peek",
    182         help = "Do not consume fetched documents"
    183     ).flag()
    184     private val dryRun by option().flag()
    185 
    186     class DryRun : Exception()
    187 
    188     override fun run() = cliCmd(logger) {
    189         nexusConfig(config).withDb { db, cfg ->
    190             val (clientKeys, bankKeys) = expectFullKeys(cfg.ebics)
    191             val pinnedStartVal = pinnedStart
    192             val pinnedStartArg = if (pinnedStartVal != null) {
    193                 logger.debug("Pinning start date to: $pinnedStartVal")
    194                 dateToInstant(pinnedStartVal)
    195             } else null
    196             val client = EbicsClient(
    197                 cfg.ebics.host,
    198                 httpClient(),
    199                 db.ebics,
    200                 EbicsLogger(ebicsLog),
    201                 clientKeys,
    202                 bankKeys
    203             )
    204             try {
    205                 client.download(
    206                     EbicsOrder.V3(type, name, scope, messageName, messageVersion, container, option),
    207                     pinnedStartArg,
    208                     null,
    209                     peek
    210                 ) { stream ->
    211                     if (container == "ZIP") {
    212                         stream.unzipEach { fileName, xmlContent ->
    213                             println(fileName)
    214                             println(xmlContent.readText())
    215                         }
    216                     } else {
    217                         println(stream.readText())
    218                     }
    219                     if (dryRun) throw DryRun()
    220                 }
    221             } catch (e: DryRun) {
    222                 // We throw DryRun to not consume files while testing
    223             }
    224         }
    225     }
    226 }
    227 
    228 class IbanGen : CliktCommand("gen") {
    229     override fun help(context: Context) = "Generate fake IBANs for testing"
    230 
    231     private val country by option().enum<Country>().required()
    232 
    233     override fun run() {
    234         println(IBAN.rand(country))
    235     }
    236 }
    237 
    238 class IbanCmd : CliktCommand("iban") {
    239     init {
    240         subcommands(IbanGen())
    241     }
    242 
    243     override fun run() = Unit
    244 }
    245 
    246 class TestingCmd : CliktCommand("testing") {
    247     init {
    248         subcommands(IbanCmd(), FakeIncoming(), ListCmd(), EbicsDownload(), TxCheck(), Wss())
    249     }
    250 
    251     override fun help(context: Context) = "Testing helper commands"
    252 
    253     override fun run() = Unit
    254 }