libeufin

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

helpers.kt (10698B)


      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 import io.ktor.client.*
     21 import io.ktor.client.request.*
     22 import io.ktor.client.statement.*
     23 import io.ktor.http.*
     24 import io.ktor.server.testing.*
     25 import kotlinx.coroutines.runBlocking
     26 import tech.libeufin.common.*
     27 import tech.libeufin.common.db.dbInit
     28 import tech.libeufin.common.db.pgDataSource
     29 import tech.libeufin.ebics.*
     30 import tech.libeufin.nexus.*
     31 import tech.libeufin.nexus.cli.registerIncomingPayment
     32 import tech.libeufin.nexus.cli.registerOutgoingPayment
     33 import tech.libeufin.nexus.db.Database
     34 import tech.libeufin.nexus.db.InitiatedPayment
     35 import tech.libeufin.nexus.db.TransferDAO.RegistrationResult
     36 import tech.libeufin.nexus.iso20022.*
     37 import java.time.Instant
     38 import kotlin.io.path.Path
     39 import kotlin.test.assertEquals
     40 
     41 fun conf(
     42     conf: String = "test.conf",
     43     lambda: suspend (NexusConfig) -> Unit
     44 ) = runBlocking {
     45     val cfg = nexusConfig(Path("conf/$conf"))
     46     lambda(cfg) 
     47 }
     48 
     49 fun setup(
     50     conf: String = "test.conf",
     51     lambda: suspend (Database, NexusConfig) -> Unit
     52 ) = conf(conf) { cfg ->
     53     pgDataSource(cfg.dbCfg.dbConnStr).dbInit(cfg.dbCfg, "libeufin-nexus", true)
     54     cfg.withDb(lambda)
     55 }
     56 
     57 fun serverSetup(
     58     conf: String = "test.conf",
     59     lambda: suspend ApplicationTestBuilder.(Database) -> Unit
     60 ) = setup(conf) { db, cfg ->
     61     testApplication {
     62         application {
     63             nexusApi(db, cfg)
     64         }
     65         lambda(db)
     66     }
     67 }
     68 
     69 const val grothoffPayto = "payto://iban/CH4189144589712575493?receiver-name=Grothoff%20Hans"
     70 
     71 val clientKeys = generateNewKeys()
     72 
     73 /** Generates a payment initiation, given its subject */
     74 fun genInitPay(
     75     endToEndId: String,
     76     subject: String = "init payment",
     77     amount: String = "KUDOS:44",
     78     creditor: IbanPayto = ibanPayto("CH4189144589712575493", "Test")
     79 ) = InitiatedPayment(
     80         id = -1,
     81         amount = TalerAmount(amount),
     82         creditor = creditor,
     83         subject = subject,
     84         initiationTime = Instant.now(),
     85         endToEndId = endToEndId
     86     )
     87 
     88 /** Generates an incoming payment, given its subject */
     89 fun genInPay(
     90     subject: String,
     91     amount: String = "KUDOS:44",
     92     executionTime: Instant = Instant.now()
     93 ) = IncomingPayment(
     94         amount = TalerAmount(amount),
     95         debtor = ibanPayto("DE84500105177118117964", "John Smith"),
     96         subject = subject,
     97         executionTime = executionTime,
     98         id = IncomingId(null, randEbicsId(), null)
     99     )
    100 
    101 /** Generates an outgoing payment, given its subject and end-to-end ID */
    102 fun genOutPay(
    103     subject: String, 
    104     endToEndId: String? = null,
    105     msgId: String? = null,
    106     executionTime: Instant = Instant.now()
    107 ) = OutgoingPayment(
    108         id = OutgoingId(msgId, endToEndId ?: randEbicsId(), null),
    109         amount = TalerAmount(44, 0, "KUDOS"),
    110         creditor = ibanPayto("CH4189144589712575493", "Test"),
    111         subject = subject,
    112         executionTime = executionTime,
    113     )
    114 
    115 /** Perform a taler outgoing transaction */
    116 suspend fun ApplicationTestBuilder.transfer() {
    117     client.postA("/taler-wire-gateway/transfer") {
    118         json {
    119             "request_uid" to HashCode.rand()
    120             "amount" to "CHF:55"
    121             "exchange_base_url" to "http://exchange.example.com/"
    122             "wtid" to ShortHashCode.rand()
    123             "credit_account" to grothoffPayto
    124         }
    125     }.assertOk()
    126 }
    127 
    128 /** Perform a taler incoming transaction of [amount] from merchant to exchange */
    129 suspend fun ApplicationTestBuilder.addIncoming(amount: String) {
    130     client.postA("/taler-wire-gateway/admin/add-incoming") {
    131         json {
    132             "amount" to TalerAmount(amount)
    133             "reserve_pub" to EddsaPublicKey.randEdsaKey()
    134             "debit_account" to grothoffPayto
    135         }
    136     }.assertOk()
    137 }
    138 
    139 /** Perform a taler kyc transaction of [amount] from merchant to exchange */
    140 suspend fun ApplicationTestBuilder.addKyc(amount: String) {
    141     client.postA("/taler-wire-gateway/admin/add-kycauth") {
    142         json {
    143             "amount" to TalerAmount(amount)
    144             "account_pub" to EddsaPublicKey.randEdsaKey()
    145             "debit_account" to grothoffPayto
    146         }
    147     }.assertOk()
    148 }
    149 
    150 /** Register a talerable outgoing transaction */
    151 suspend fun talerableOut(db: Database, metadata: String? = null) {
    152     val wtid = EddsaPublicKey.randEdsaKey()
    153     registerOutgoingPayment(db, genOutPay(fmtOutgoingSubject(wtid, BaseURL.parse("http://exchange.example.com/"), metadata)))
    154 }
    155 
    156 /** Register a talerable reserve incoming transaction */
    157 suspend fun talerableIn(
    158     db: Database,
    159     amount: String = "CHF:44",
    160     reserve_pub: EddsaPublicKey = EddsaPublicKey.randEdsaKey()
    161 ) {
    162     registerIncomingPayment(
    163         db, NexusIngestConfig.default(AccountType.exchange),
    164         genInPay("test with $reserve_pub reserve pub", amount)
    165     )
    166 }
    167 
    168 private suspend fun prepare(db: Database): String {
    169     val pub = EddsaPublicKey.randEdsaKey()
    170     val sig = EddsaSignature.rand()
    171     val referenceNumber = subjectFmtQrBill(pub)
    172     assertEquals(
    173         RegistrationResult.Success,
    174         db.transfer.register(
    175             type = TransferType.reserve,
    176             accountPub = pub,
    177             authPub = pub,
    178             authSig = sig,
    179             referenceNumber = referenceNumber,
    180             timestamp = Instant.now(),
    181             recurrent = false
    182         )
    183     )
    184     return referenceNumber
    185 }
    186 
    187 /** Register a talerable reserve prepared incoming transaction */
    188 suspend fun talerablePreparedIn(db: Database, amount: String = "CHF:44") {
    189     val referenceNumber = prepare(db)
    190     registerIncomingPayment(
    191         db, NexusIngestConfig.default(AccountType.exchange),
    192         genInPay(referenceNumber, amount)
    193     )
    194 }
    195 
    196 /** Register an incomplete talerable reserve prepared incoming transaction */
    197 suspend fun talerablePreparedIncompleteIn(db: Database, amount: String = "CHF:44") {
    198     val referenceNumber = prepare(db)
    199     val incomplete = genInPay(referenceNumber).copy(subject = null, debtor = null)
    200     registerIncomingPayment(
    201         db, NexusIngestConfig.default(AccountType.exchange), incomplete
    202     )
    203 }
    204 
    205 /** Register a completed talerable reserve prepared incoming transaction */
    206 suspend fun talerablePreparedCompletedIn(db: Database, amount: String = "CHF:44") {
    207     val referenceNumber = prepare(db)
    208     val original = genInPay(referenceNumber, amount)
    209     val incomplete = original.copy(subject = null, debtor = null)
    210     registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), incomplete)
    211     registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), original)
    212 }
    213 
    214 /** Register an incomplete talerable reserve incoming transaction */
    215 suspend fun talerableIncompleteIn(db: Database) {
    216     val reserve_pub = EddsaPublicKey.randEdsaKey()
    217     val incomplete = genInPay("test with $reserve_pub reserve pub").copy(subject = null, debtor = null)
    218     registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), incomplete)
    219 }
    220 
    221 /** Register a completed talerable reserve  incoming transaction */
    222 suspend fun talerableCompletedIn(db: Database) {
    223     val reserve_pub = EddsaPublicKey.randEdsaKey()
    224     val original = genInPay("test with $reserve_pub reserve pub")
    225     val incomplete = original.copy(subject = null, debtor = null)
    226     registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), incomplete)
    227     registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), original)
    228 }
    229 
    230 /** Register a talerable KYC incoming transaction */
    231 suspend fun talerableKycIn(
    232     db: Database,
    233     amount: String = "CHF:44",
    234     account_pub: EddsaPublicKey = EddsaPublicKey.randEdsaKey()
    235 ) {
    236     registerIncomingPayment(
    237         db, NexusIngestConfig.default(AccountType.exchange),
    238         genInPay("test with KYC:$account_pub account pub", amount)
    239     )
    240 }
    241 
    242 /** Register an incoming transaction */
    243 suspend fun registerIn(db: Database) {
    244     registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), genInPay("ignored"))
    245 }
    246 
    247 /** Register an incomplete incoming transaction */
    248 suspend fun registerIncompleteIn(db: Database) {
    249     val incomplete = genInPay("ignored").copy(subject = null, debtor = null)
    250     registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), incomplete)
    251 }
    252 
    253 /** Register a completed incoming transaction */
    254 suspend fun registerCompletedIn(db: Database) {
    255     val original = genInPay("ignored")
    256     val incomplete = original.copy(subject = null, debtor = null)
    257     registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), incomplete)
    258     registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), original)
    259 }
    260 
    261 /** Register an outgoing transaction */
    262 suspend fun registerOut(db: Database) {
    263     registerOutgoingPayment(db, genOutPay("ignored"))
    264 }
    265 
    266 /** Register an incomplete outgoing transaction */
    267 suspend fun registerIncompleteOut(db: Database) {
    268     val original = genOutPay("ignored")
    269     val incomplete = original.copy(id = OutgoingId(null, null, original.id.endToEndId), creditor = null)
    270     registerOutgoingPayment(db, incomplete)
    271 }
    272 
    273 /* ----- Auth ----- */
    274 
    275 /** Auto auth get request */
    276 suspend inline fun HttpClient.getA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse {
    277     return get(url) {
    278         auth()
    279         builder(this)
    280     }
    281 }
    282 
    283 /** Auto auth post request */
    284 suspend inline fun HttpClient.postA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse {
    285     return post(url) {
    286         auth()
    287         builder(this)
    288     }
    289 }
    290 
    291 /** Auto auth patch request */
    292 suspend inline fun HttpClient.patchA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse {
    293     return patch(url) {
    294         auth()
    295         builder(this)
    296     }
    297 }
    298 
    299 /** Auto auth delete request */
    300 suspend inline fun HttpClient.deleteA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse {
    301     return delete(url) {
    302         auth()
    303         builder(this)
    304     }
    305 }
    306 
    307 fun HttpRequestBuilder.auth() {
    308     headers[HttpHeaders.Authorization] = "Bearer secret-token"
    309 }