libeufin

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

PreparedTransferApiTest.kt (12056B)


      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 PreparedTransferApiTest {
     29     // GET /taler-prepared-transfer/config
     30     @Test
     31     fun config() = bankSetup {
     32         client.get("/taler-prepared-transfer/config").assertOkJson<PreparedTransferConfig>()
     33     }
     34 
     35     // POST /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 = SubjectRequest(
     41             exchangePayto,
     42             TransferType.reserve,
     43             false,
     44             amount,
     45             PublicKeyAlg.EdDSA,
     46             pub,
     47             pub,
     48             EddsaSignature.rand()
     49         )
     50 
     51         val simpleSubject = TransferSubject.Simple("Taler MAP:$pub", amount)
     52 
     53         // Valid
     54         val subjects = client.post("/taler-prepared-transfer/registration") {
     55             json(valid_req.sign(priv))
     56         }.assertOkJson<SubjectResult> {
     57             assertEquals(it.subjects[1], simpleSubject)
     58             assertIs<TransferSubject.Uri>(it.subjects[0])
     59         }.subjects
     60 
     61         // Idempotent
     62         client.post("/taler-prepared-transfer/registration") {
     63             json(valid_req.sign(priv))
     64         }.assertOkJson<SubjectResult> {
     65             assertEquals(it.subjects, subjects)
     66         }
     67 
     68         // KYC has a different withdrawal uri
     69         client.post("/taler-prepared-transfer/registration") {
     70             json(valid_req.copy(type = TransferType.kyc).sign(priv))
     71         }.assertOkJson<SubjectResult> {
     72             assertEquals(it.subjects[1], simpleSubject)
     73             val uriSubject = assertIs<TransferSubject.Uri>(it.subjects[0])
     74             assertNotEquals(subjects[0], uriSubject)
     75         }
     76 
     77         // Recurrent only has simple subject
     78         client.post("/taler-prepared-transfer/registration") {
     79             json(valid_req.copy(recurrent = true).sign(priv))
     80         }.assertOkJson<SubjectResult> {
     81             assertEquals(it.subjects, listOf(simpleSubject))
     82         }
     83 
     84         // Bad signature
     85         client.post("/taler-prepared-transfer/registration") {
     86             json(valid_req)
     87         }.assertForbidden(TalerErrorCode.BANK_BAD_SIGNATURE)
     88 
     89         // Not exchange
     90         client.post("/taler-prepared-transfer/registration") {
     91             json(valid_req.copy(credit_account = merchantPayto).sign(priv))
     92         }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE)
     93 
     94         // Unknown account
     95         client.post("/taler-prepared-transfer/registration") {
     96             json(valid_req.copy(credit_account = unknownPayto).sign(priv))
     97         }.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR)
     98 
     99         assertBalance("customer", "+KUDOS:0")
    100         assertBalance("exchange", "+KUDOS:0")
    101 
    102         // Non recurrent accept on then bounce
    103         client.post("/taler-prepared-transfer/registration") {
    104             json(valid_req.sign(priv))
    105         }.assertOkJson<SubjectResult> {
    106             val uuid = (it.subjects[0] as? TransferSubject.Uri)!!.uri.substringAfterLast('/')
    107             client.postA("/accounts/customer/withdrawals/$uuid/confirm").assertNoContent() // reserve
    108             tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce
    109             tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce
    110             assertBalance("customer", "-KUDOS:1")
    111             assertBalance("exchange", "+KUDOS:1")
    112         }
    113 
    114         // Withdrawal is aborted on completion
    115         client.post("/taler-prepared-transfer/registration") {
    116             json(valid_req.copy(type = TransferType.kyc).sign(priv))
    117         }.assertOkJson<SubjectResult> {
    118             val uuid = (it.subjects[0] as? TransferSubject.Uri)!!.uri.substringAfterLast('/')
    119             println("UUID $uuid")
    120             tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // kyc
    121             tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce
    122             client.postA("/accounts/customer/withdrawals/$uuid/confirm")
    123                 .assertConflict(TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT) // aborted
    124             assertBalance("customer", "-KUDOS:2")
    125             assertBalance("exchange", "+KUDOS:2")
    126         }
    127 
    128         // Recurrent accept one and delay others
    129         val newKey = EddsaPublicKey.randEdsaKey()
    130         client.post("/taler-prepared-transfer/registration") {
    131             json(valid_req.copy(account_pub = newKey, recurrent = true).sign(priv))
    132         }
    133         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve
    134         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
    135         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
    136         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
    137         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
    138         assertBalance("customer", "-KUDOS:7")
    139         assertBalance("exchange", "+KUDOS:7")
    140 
    141         // Complete pending on recurrent update
    142         val kycKey = EddsaPublicKey.randEdsaKey()
    143         client.post("/taler-prepared-transfer/registration") {
    144             json(valid_req.copy(type= TransferType.kyc, account_pub = kycKey, recurrent = true).sign(priv))
    145         }.assertOkJson<SubjectResult>()
    146         client.post("/taler-prepared-transfer/registration") {
    147             json(valid_req.copy(account_pub = kycKey, recurrent = true).sign(priv))
    148         }.assertOkJson<SubjectResult>()
    149         assertBalance("customer", "-KUDOS:7")
    150         assertBalance("exchange", "+KUDOS:7")
    151 
    152         // Kyc key reuse keep pending ones
    153         tx("customer", "KUDOS:1", "exchange", "Taler KYC:$kycKey")
    154         assertBalance("customer", "-KUDOS:8")
    155         assertBalance("exchange", "+KUDOS:8")
    156 
    157         // Switching to non recurrent cancel pending
    158         client.post("/taler-prepared-transfer/registration") {
    159             json(valid_req.copy(type= TransferType.kyc, account_pub = kycKey).sign(priv))
    160         }.assertOkJson<SubjectResult>()
    161         assertBalance("customer", "-KUDOS:6")
    162         assertBalance("exchange", "+KUDOS:6")
    163 
    164         // Check authorization field in incoming history
    165         val (testPriv, testAuth) = EddsaPublicKey.randEdsaKeyPair()
    166         val testKey = EddsaPublicKey.randEdsaKey()
    167         val qr = subjectFmtQrBill(testAuth)
    168         client.post("/taler-prepared-transfer/registration") {
    169             json(valid_req.copy(account_pub=testKey, authorization_pub=testAuth, recurrent=true).sign(testPriv))
    170         }.assertOkJson<SubjectResult>()
    171         tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth")
    172         tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth")
    173         tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth")
    174         client.post("/taler-prepared-transfer/registration") {
    175             json(valid_req.copy(type=TransferType.kyc, account_pub=testKey, authorization_pub=testAuth, recurrent=true).sign(testPriv))
    176         }.assertOkJson<SubjectResult>()
    177         val otherPub = EddsaPublicKey.randEdsaKey()
    178         client.post("/taler-prepared-transfer/registration") {
    179             json(valid_req.copy(account_pub=otherPub, authorization_pub=testAuth, recurrent=true).sign(testPriv))
    180         }.assertOkJson<SubjectResult>()
    181         val lastPub = EddsaPublicKey.randEdsaKey()
    182         tx("customer", "KUDOS:0.1", "exchange", "Taler $lastPub")
    183         tx("customer", "KUDOS:0.1", "exchange", "Taler KYC:$lastPub")
    184         val history = client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?limit=-5")
    185             .assertOkJson<IncomingHistory>().incoming_transactions.map {
    186                 when (it) {
    187                     is IncomingKycAuthTransaction -> Pair(it.account_pub, it.authorization_pub)
    188                     is IncomingReserveTransaction -> Pair(it.reserve_pub, it.authorization_pub)
    189                     else -> throw UnsupportedOperationException()
    190                 }
    191             }
    192         assertContentEquals(history, listOf(
    193             Pair(lastPub, null),
    194             Pair(lastPub, null),
    195             Pair(otherPub, testAuth),
    196             Pair(testKey, testAuth),
    197             Pair(testKey, testAuth)
    198         ))
    199     }
    200 
    201     // DELETE /taler-prepared-transfer/registration
    202     @Test
    203     fun unregistration() = bankSetup {
    204         val (priv, pub) = EddsaPublicKey.randEdsaKeyPair()
    205         val valid_req = SubjectRequest(
    206             exchangePayto,
    207             TransferType.reserve,
    208             false,
    209             TalerAmount("KUDOS:1"),
    210             PublicKeyAlg.EdDSA,
    211             pub,
    212             pub,
    213             EddsaSignature.rand()
    214         ).sign(priv)
    215 
    216         val now = TalerTimestamp(Instant.now())
    217         val req = Unregistration(
    218             now,
    219             pub,
    220             EddsaSignature.rand()
    221         ).sign(priv)
    222       
    223         // Unknown
    224         client.post("/taler-prepared-transfer/unregistration") {
    225             json(req)
    226         }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
    227         
    228         // Know
    229         client.post("/taler-prepared-transfer/registration") {
    230             json(valid_req)
    231         }.assertOkJson<SubjectResult>()
    232         client.post("/taler-prepared-transfer/unregistration") {
    233             json(req)
    234         }.assertNoContent()
    235 
    236         // Idempotent
    237         client.post("/taler-prepared-transfer/unregistration") {
    238             json(req)
    239         }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
    240 
    241         // Bad signature
    242         client.post("/taler-prepared-transfer/unregistration") {
    243             json(
    244                 Unregistration(
    245                     now,
    246                     pub,
    247                     EddsaSignature.rand()
    248                 )
    249             )
    250         }.assertForbidden(TalerErrorCode.BANK_BAD_SIGNATURE)
    251 
    252         // Old timestamp
    253         client.post("/taler-prepared-transfer/unregistration") {
    254             json(
    255                 Unregistration(
    256                     TalerTimestamp(Instant.now().minusSeconds(1000000)),
    257                     pub,
    258                     EddsaSignature.rand()
    259                 ).sign(priv)
    260             )
    261         }.assertConflict(TalerErrorCode.BANK_OLD_TIMESTAMP)
    262 
    263         // Unknown bounce
    264         assertBalance("customer", "+KUDOS:0")
    265         assertBalance("exchange", "+KUDOS:0")
    266         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce
    267         assertBalance("customer", "+KUDOS:0")
    268         assertBalance("exchange", "+KUDOS:0")
    269 
    270         // Pending bounced after deletion
    271         val newKey = EddsaPublicKey.randEdsaKey()
    272         client.post("/taler-prepared-transfer/registration") {
    273             json(valid_req.copy(account_pub=newKey, recurrent=true).sign(priv))
    274         }.assertOkJson<SubjectResult>()
    275         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve
    276         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
    277         tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
    278         assertBalance("customer", "-KUDOS:3")
    279         assertBalance("exchange", "+KUDOS:3")
    280         client.post("/taler-prepared-transfer/unregistration") {
    281             json(req)
    282         }.assertNoContent()
    283         assertBalance("customer", "-KUDOS:1")
    284         assertBalance("exchange", "+KUDOS:1")
    285     }
    286 }