libeufin

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

PreparedApiTest.kt (14709B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 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.request.*
     21 import org.junit.Test
     22 import tech.libeufin.common.*
     23 import tech.libeufin.common.crypto.CryptoUtil
     24 import tech.libeufin.common.test.*
     25 import java.time.Instant
     26 import kotlin.test.*
     27 
     28 class WireTransferApiTest {
     29     // GET /accounts/{USERNAME}/taler-prepared-transfer/config
     30     @Test
     31     fun config() = bankSetup {
     32         client.get("/accounts/merchant/taler-prepared-transfer/config").assertOkJson<PreparedTransferConfig>()
     33     }
     34 
     35     // POST /accounts/{USERNAME}/taler-prepared-transfer/registration
     36     @Test
     37     fun registration() = bankSetup {
     38         val (priv, pub) = EddsaPublicKey.randEdsaKeyPair()
     39         val amount = TalerAmount("KUDOS:1")
     40         val valid_req = obj {
     41             "credit_amount" to amount
     42             "type" to "reserve"
     43             "alg" to "EdDSA"
     44             "account_pub" to pub
     45             "authorization_pub" to pub
     46             "authorization_sig" to CryptoUtil.eddsaSign(pub.raw, priv)
     47             "recurrent" to false
     48         }
     49 
     50         val simpleSubject = TransferSubject.Simple("Taler MAP:$pub", amount)
     51 
     52         // Valid
     53         val subjects = client.post("/accounts/exchange/taler-prepared-transfer/registration") {
     54             json(valid_req)
     55         }.assertOkJson<SubjectResult> {
     56             assertEquals(it.subjects[1], simpleSubject)
     57             assertIs<TransferSubject.Uri>(it.subjects[0])
     58         }.subjects
     59 
     60         // Idempotent
     61         client.post("/accounts/exchange/taler-prepared-transfer/registration") {
     62             json(valid_req)
     63         }.assertOkJson<SubjectResult> {
     64             assertEquals(it.subjects, subjects)
     65         }
     66 
     67         // KYC has a different withdrawal uri
     68         client.post("/accounts/exchange/taler-prepared-transfer/registration") {
     69             json(valid_req) {
     70                 "type" to "kyc"
     71             }
     72         }.assertOkJson<SubjectResult> {
     73             assertEquals(it.subjects[1], simpleSubject)
     74             val uriSubject = assertIs<TransferSubject.Uri>(it.subjects[0])
     75             assertNotEquals(subjects[0], uriSubject)
     76         }
     77 
     78         // Recurrent only has simple subject
     79         client.post("/accounts/exchange/taler-prepared-transfer/registration") {
     80             json(valid_req) {
     81                 "recurrent" to true
     82             }
     83         }.assertOkJson<SubjectResult> {
     84             assertEquals(it.subjects, listOf(simpleSubject))
     85         }
     86 
     87         // Bad signature
     88         client.post("/accounts/exchange/taler-prepared-transfer/registration") {
     89             json(valid_req) {
     90                 "authorization_sig" to EddsaSignature.rand()
     91             }
     92         }.assertConflict(TalerErrorCode.BANK_BAD_SIGNATURE)
     93 
     94         // Not exchange
     95         client.post("/accounts/merchant/taler-prepared-transfer/registration") {
     96             json(valid_req)
     97         }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE)
     98 
     99         // Unknown account
    100         client.post("/accounts/unknown/taler-prepared-transfer/registration") {
    101             json(valid_req)
    102         }.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT)
    103 
    104         assertBalance("customer", "+KUDOS:0")
    105         assertBalance("exchange", "+KUDOS:0")
    106 
    107         // Non recurrent accept on then bounce
    108         client.post("/accounts/exchange/taler-prepared-transfer/registration") {
    109             json(valid_req) {
    110                 "type" to "reserve"
    111             }
    112         }.assertOkJson<SubjectResult> {
    113             val uuid = (it.subjects[0] as? TransferSubject.Uri)!!.uri.substringAfterLast('/')
    114             client.postA("/accounts/customer/withdrawals/$uuid/confirm").assertNoContent() // reserve
    115             tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce
    116             tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce
    117             assertBalance("customer", "-KUDOS:1")
    118             assertBalance("exchange", "+KUDOS:1")
    119         }
    120 
    121         // Withdrawal is aborted on completion
    122         client.post("/accounts/exchange/taler-prepared-transfer/registration") {
    123             json(valid_req) {
    124                 "type" to "kyc"
    125             }
    126         }.assertOkJson<SubjectResult> {
    127             val uuid = (it.subjects[0] as? TransferSubject.Uri)!!.uri.substringAfterLast('/')
    128             println("UUID $uuid")
    129             tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // kyc
    130             tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce
    131             client.postA("/accounts/customer/withdrawals/$uuid/confirm")
    132                 .assertConflict(TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT) // aborted
    133             assertBalance("customer", "-KUDOS:2")
    134             assertBalance("exchange", "+KUDOS:2")
    135         }
    136 
    137         // Recurrent accept one and delay others
    138         val newKey = EddsaPublicKey.randEdsaKey()
    139         client.post("/accounts/exchange/taler-prepared-transfer/registration") {
    140             json(valid_req) {
    141                 "account_pub" to newKey
    142                 "authorization_sig" to CryptoUtil.eddsaSign(newKey.raw, priv)
    143                 "recurrent" to true
    144             }
    145         }
    146         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve
    147         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
    148         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
    149         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
    150         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
    151         assertBalance("customer", "-KUDOS:7")
    152         assertBalance("exchange", "+KUDOS:7")
    153 
    154         // Complete pending on recurrent update
    155         val kycKey = EddsaPublicKey.randEdsaKey()
    156         client.post("/accounts/exchange/taler-prepared-transfer/registration") {
    157             json(valid_req) {
    158                 "type" to "kyc"
    159                 "account_pub" to kycKey
    160                 "authorization_sig" to CryptoUtil.eddsaSign(kycKey.raw, priv)
    161                 "recurrent" to true
    162             }
    163         }.assertOkJson<SubjectResult>()
    164         client.post("/accounts/exchange/taler-prepared-transfer/registration") {
    165             json(valid_req) {
    166                 "type" to "reserve"
    167                 "account_pub" to kycKey
    168                 "authorization_sig" to CryptoUtil.eddsaSign(kycKey.raw, priv)
    169                 "recurrent" to true
    170             }
    171         }.assertOkJson<SubjectResult>()
    172         assertBalance("customer", "-KUDOS:7")
    173         assertBalance("exchange", "+KUDOS:7")
    174 
    175         // Kyc key reuse keep pending ones
    176         tx("customer", "KUDOS:1", "exchange", "Taler KYC:$kycKey")
    177         assertBalance("customer", "-KUDOS:8")
    178         assertBalance("exchange", "+KUDOS:8")
    179 
    180         // Switching to non recurrent cancel pending
    181         client.post("/accounts/exchange/taler-prepared-transfer/registration") {
    182             json(valid_req) {
    183                 "type" to "kyc"
    184                 "account_pub" to kycKey
    185                 "authorization_sig" to CryptoUtil.eddsaSign(kycKey.raw, priv)
    186             }
    187         }.assertOkJson<SubjectResult>()
    188         assertBalance("customer", "-KUDOS:6")
    189         assertBalance("exchange", "+KUDOS:6")
    190 
    191         // Check authorization field in incoming history
    192         val (testPriv, testAuth) = EddsaPublicKey.randEdsaKeyPair()
    193         val testKey = EddsaPublicKey.randEdsaKey()
    194         val testSig = CryptoUtil.eddsaSign(testKey.raw, testPriv)
    195         val qr = subjectFmtQrBill(testAuth)
    196         client.post("/accounts/exchange/taler-prepared-transfer/registration") {
    197             json(valid_req) {
    198                 "account_pub" to testKey
    199                 "authorization_pub" to testAuth
    200                 "authorization_sig" to testSig
    201                 "recurrent" to true
    202             }
    203         }.assertOkJson<SubjectResult>()
    204         tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth")
    205         tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth")
    206         tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth")
    207         client.post("/accounts/exchange/taler-prepared-transfer/registration") {
    208             json(valid_req) {
    209                 "type" to "kyc"
    210                 "account_pub" to testKey
    211                 "authorization_pub" to testAuth
    212                 "authorization_sig" to testSig
    213                 "recurrent" to true
    214             }
    215         }.assertOkJson<SubjectResult>()
    216         val otherPub = EddsaPublicKey.randEdsaKey()
    217         val otherSig = CryptoUtil.eddsaSign(otherPub.raw, testPriv)
    218         client.post("/accounts/exchange/taler-prepared-transfer/registration") {
    219             json(valid_req) {
    220                 "type" to "reserve"
    221                 "account_pub" to otherPub
    222                 "authorization_pub" to testAuth
    223                 "authorization_sig" to otherSig
    224                 "recurrent" to true
    225             }
    226         }.assertOkJson<SubjectResult>()
    227         val lastPub = EddsaPublicKey.randEdsaKey()
    228         tx("customer", "KUDOS:0.1", "exchange", "Taler $lastPub")
    229         tx("customer", "KUDOS:0.1", "exchange", "Taler KYC:$lastPub")
    230         val history = client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?limit=-5")
    231             .assertOkJson<IncomingHistory>().incoming_transactions.map {
    232                 when (it) {
    233                     is IncomingKycAuthTransaction -> Triple(it.account_pub, it.authorization_pub, it.authorization_sig)
    234                     is IncomingReserveTransaction -> Triple(it.reserve_pub, it.authorization_pub, it.authorization_sig)
    235                     else -> throw UnsupportedOperationException()
    236                 }
    237             }
    238         assertContentEquals(history, listOf(
    239             Triple(lastPub, null, null),
    240             Triple(lastPub, null, null),
    241             Triple(otherPub, testAuth, otherSig),
    242             Triple(testKey, testAuth, testSig),
    243             Triple(testKey, testAuth, testSig)
    244         ))
    245     }
    246 
    247     // DELETE /accounts/{USERNAME}/taler-prepared-transfer/registration
    248     @Test
    249     fun unregistration() = bankSetup {
    250         val (priv, pub) = EddsaPublicKey.randEdsaKeyPair()
    251         val valid_req = obj {
    252             "credit_amount" to "KUDOS:1"
    253             "type" to "reserve"
    254             "alg" to "EdDSA"
    255             "account_pub" to pub
    256             "authorization_pub" to pub
    257             "authorization_sig" to CryptoUtil.eddsaSign(pub.raw, priv)
    258             "recurrent" to false
    259         }
    260 
    261         // Unknown
    262         client.delete("/accounts/exchange/taler-prepared-transfer/registration") {
    263             val now = Instant.now().toString()
    264             json {
    265                 "timestamp" to now
    266                 "authorization_pub" to pub
    267                 "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv)
    268             }
    269         }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
    270                 
    271         // Know
    272         client.post("/accounts/exchange/taler-prepared-transfer/registration") {
    273             json(valid_req)
    274         }.assertOkJson<SubjectResult>()
    275         client.delete("/accounts/exchange/taler-prepared-transfer/registration") {
    276             val now = Instant.now().toString()
    277             json {
    278                 "timestamp" to now
    279                 "authorization_pub" to pub
    280                 "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv)
    281             }
    282         }.assertNoContent()
    283 
    284         // Idempotent
    285         client.delete("/accounts/exchange/taler-prepared-transfer/registration") {
    286             val now = Instant.now().toString()
    287             json {
    288                 "timestamp" to now
    289                 "authorization_pub" to pub
    290                 "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv)
    291             }
    292         }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
    293 
    294         // Bad signature
    295         client.delete("/accounts/exchange/taler-prepared-transfer/registration") {
    296             val now = Instant.now().toString()
    297             json {
    298                 "timestamp" to now
    299                 "authorization_pub" to pub
    300                 "authorization_sig" to CryptoUtil.eddsaSign("lol".toByteArray(), priv)
    301             }
    302         }.assertConflict(TalerErrorCode.BANK_BAD_SIGNATURE)
    303 
    304         // Old timestamp
    305         client.delete("/accounts/exchange/taler-prepared-transfer/registration") {
    306             val now = Instant.now().minusSeconds(1000000).toString()
    307             json {
    308                 "timestamp" to now
    309                 "authorization_pub" to pub
    310                 "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv)
    311             }
    312         }.assertConflict(TalerErrorCode.BANK_OLD_TIMESTAMP)
    313 
    314         // Unknown bounce
    315         assertBalance("customer", "+KUDOS:0")
    316         assertBalance("exchange", "+KUDOS:0")
    317         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce
    318         assertBalance("customer", "+KUDOS:0")
    319         assertBalance("exchange", "+KUDOS:0")
    320 
    321         // Pending bounced after deletion
    322         val newKey = EddsaPublicKey.randEdsaKey()
    323         client.post("/accounts/exchange/taler-prepared-transfer/registration") {
    324             json(valid_req) {
    325                 "account_pub" to newKey
    326                 "authorization_sig" to CryptoUtil.eddsaSign(newKey.raw, priv)
    327                 "recurrent" to true
    328             }
    329         }.assertOkJson<SubjectResult>()
    330         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve
    331         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
    332         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
    333         assertBalance("customer", "-KUDOS:3")
    334         assertBalance("exchange", "+KUDOS:3")
    335         client.delete("/accounts/exchange/taler-prepared-transfer/registration") {
    336             val now = Instant.now().toString()
    337             json {
    338                 "timestamp" to now
    339                 "authorization_pub" to pub
    340                 "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv)
    341             }
    342         }.assertNoContent()
    343         assertBalance("customer", "-KUDOS:1")
    344         assertBalance("exchange", "+KUDOS:1")
    345     }
    346 }