libeufin

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

WireTransferApiTest.kt (14893B)


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